Files
ewoooc/templates/code_review.html
ogt 2144ef2102
All checks were successful
CD Pipeline / deploy (push) Successful in 1m11s
fix: lock pchome growth ui copy guardrails
2026-06-26 11:38:04 +08:00

625 lines
28 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>
.code-review-page {
--bg: var(--momo-bg-paper);
--panel: var(--momo-bg-surface);
--border: var(--momo-border-light);
--text: var(--momo-text-primary);
--muted: var(--momo-text-secondary);
--red: var(--momo-danger);
--orange: var(--momo-warm-clay);
--yellow: var(--momo-warning);
--green: var(--momo-success);
--blue: var(--momo-info);
--purple: var(--momo-warm-saffron);
--accent: var(--momo-page-accent);
color: var(--text);
font-family: var(--momo-font-family);
font-size: 14px;
}
.code-review-page * { box-sizing: border-box; }
/* ── Layout ─────────────────────────────────────────── */
.topbar { background: var(--momo-dot-grid), var(--panel); border: 1px solid var(--momo-border-strong); border-radius: var(--momo-radius-md); margin-bottom: 12px; padding: 14px 18px; display: flex; align-items: center; gap: 12px; }
.topbar h1 { font-size: 18px; font-weight: 800; color: var(--text); margin: 0; letter-spacing: 0; }
.topbar .badge { font-size: 11px; padding: 3px 8px; border-radius: var(--momo-radius-sm); background: var(--momo-tag-terra-bg); border: 1px solid var(--momo-tag-terra-border); color: var(--momo-tag-terra-text); }
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 1.5s infinite; margin-left: auto; }
.live-dot.idle { background: var(--muted); animation: none; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
.layout { display: grid; grid-template-columns: minmax(280px, 340px) minmax(0, 1fr); gap: 12px; min-height: 680px; overflow: visible; }
.sidebar { background: transparent; overflow: visible; }
.main { overflow: visible; min-width: 0; }
.layout,
.sidebar,
.main,
.card,
.card-header,
.card-body {
min-width: 0;
}
/* ── Card ───────────────────────────────────────────── */
.card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--momo-radius-md); margin-bottom: 12px; overflow: hidden; box-shadow: none; }
.card-header { padding: 10px 14px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 8px; }
.card-body { padding: 14px; }
/* ── Pipeline Steps ──────────────────────────────────── */
.pipeline { display: flex; flex-direction: column; gap: 6px; }
.step { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--momo-radius-sm); border: 1px solid var(--border); background: var(--momo-bg-paper); transition: border-color .3s; }
.step.running { border-color: var(--blue); background: var(--momo-info-bg); }
.step.ok { border-color: var(--green); background: var(--momo-success-bg); }
.step.error { border-color: var(--red); background: var(--momo-danger-bg); }
.step-num { width: 22px; height: 22px; border-radius: var(--momo-radius-sm); background: var(--momo-bg-subtle); font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.step.ok .step-num,
.step.running .step-num,
.step.error .step-num { color: var(--momo-text-inverse); }
.step.ok .step-num { background: var(--green); }
.step.running .step-num { background: var(--blue); }
.step.error .step-num { background: var(--red); }
.step-info { flex: 1; min-width: 0; }
.step-name { font-weight: 600; font-size: 13px; }
.step-agent { font-size: 11px; color: var(--purple); }
.step-summary { font-size: 11px; color: var(--muted); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.step-icon { font-size: 16px; }
.spinner { display: inline-block; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Severity Badges ─────────────────────────────────── */
.sev-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 8px; }
.sev-cell { text-align: center; padding: 10px 6px; border-radius: 6px; border: 1px solid var(--border); }
.sev-cell .num { font-size: 28px; font-weight: 700; line-height: 1; }
.sev-cell .lbl { font-size: 11px; color: var(--muted); margin-top: 4px; }
.sev-critical { border-color: var(--red)!important; } .sev-critical .num { color: var(--red); }
.sev-high { border-color: var(--orange)!important; } .sev-high .num { color: var(--orange); }
.sev-medium { border-color: var(--yellow)!important; } .sev-medium .num { color: var(--yellow); }
.sev-low { border-color: var(--green)!important; } .sev-low .num { color: var(--green); }
/* ── Findings Table ──────────────────────────────────── */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { padding: 8px 10px; text-align: left; color: var(--muted); font-weight: 600; border-bottom: 1px solid var(--border); font-size: 11px; text-transform: uppercase; letter-spacing: .5px; }
td { padding: 8px 10px; border-bottom: 1px solid var(--momo-border-light); vertical-align: top; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--momo-bg-paper); }
.badge-sev { display: inline-block; padding: 2px 7px; border-radius: 10px; font-size: 11px; font-weight: 700; }
.badge-CRITICAL { background: var(--momo-danger-bg); color: var(--momo-danger-text); }
.badge-HIGH { background: var(--momo-warm-clay-soft); color: var(--orange); }
.badge-MEDIUM { background: var(--momo-warning-bg); color: var(--momo-warning-text); }
.badge-LOW { background: var(--momo-success-bg); color: var(--momo-success-text); }
.badge-type { display: inline-block; padding: 1px 6px; border-radius: var(--momo-radius-sm); font-size: 10px; background: var(--momo-bg-paper); color: var(--muted); }
code { background: var(--momo-bg-paper); padding: 1px 5px; border-radius: var(--momo-radius-xs); font-size: 12px; color: var(--momo-info-text); }
/* ── Architecture Report ─────────────────────────────── */
.report-box { background: var(--momo-bg-paper); border: 1px solid var(--border); border-radius: var(--momo-radius-sm); padding: 12px; font-size: 13px; line-height: 1.6; }
.report-box b { color: var(--text); }
/* ── EA Decision ─────────────────────────────────────── */
.ea-box { padding: 12px 14px; border-radius: 6px; border-left: 4px solid var(--blue); background: var(--momo-info-bg); }
.ea-box.critical { border-left-color: var(--red); background: var(--momo-danger-bg); }
.ea-box.high { border-left-color: var(--orange); background: var(--momo-warm-clay-soft); }
.ea-box.medium { border-left-color: var(--yellow); }
.ea-box.low { border-left-color: var(--green); }
.ea-priority { font-size: 20px; font-weight: 700; }
.ea-reasoning { color: var(--muted); font-size: 12px; margin-top: 4px; }
.ea-fix { margin-top: 8px; font-size: 12px; }
/* ── History ─────────────────────────────────────────── */
.hist-item { padding: 10px; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 8px; cursor: pointer; transition: border-color .2s; }
.hist-item:hover { border-color: var(--blue); }
.hist-sha { font-family: monospace; font-size: 12px; color: var(--blue); }
.hist-meta { font-size: 11px; color: var(--muted); margin-top: 3px; }
.hist-sev { display: flex; gap: 6px; margin-top: 5px; }
.hist-sev span { font-size: 11px; padding: 1px 6px; border-radius: 8px; }
/* ── Status Bar ──────────────────────────────────────── */
#statusBar { padding: 10px 14px; border-radius: 6px; margin-bottom: 12px; font-size: 13px; display: none; }
#statusBar.running { background: var(--momo-info-bg); border: 1px solid var(--blue); }
#statusBar.completed { background: var(--momo-success-bg); border: 1px solid var(--green); }
#statusBar.error { background: var(--momo-danger-bg); border: 1px solid var(--red); }
#statusBar.skipped { background: var(--momo-bg-paper); border: 1px solid var(--border); }
/* ── Empty state ─────────────────────────────────────── */
.empty { text-align: center; padding: 40px; color: var(--muted); }
.empty .icon { font-size: 40px; margin-bottom: 10px; }
/* ── Tabs ────────────────────────────────────────────── */
.tabs { display: flex; gap: 4px; margin-bottom: 12px; }
.tab { padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--muted); border: 1px solid transparent; }
.tab.active { background: var(--momo-page-accent-soft); color: var(--text); border-color: var(--momo-page-accent-line); }
.tab-pane { display: none; }
.tab-pane.active { display: block; }
@media (max-width: 760px) {
.topbar {
flex-wrap: wrap;
padding: 10px 14px;
}
.topbar h1 {
font-size: 16px;
}
.layout {
grid-template-columns: 1fr;
height: auto;
min-height: calc(100vh - 49px);
overflow: visible;
}
.sidebar,
.main {
width: 100%;
max-width: 100%;
overflow: visible;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid var(--border);
}
.card,
.card-header,
.card-body,
.pipeline,
.step,
.step-info {
max-width: 100%;
min-width: 0;
}
.card-header {
flex-wrap: wrap;
overflow-wrap: anywhere;
}
#pipelineId {
flex: 1 1 100%;
margin-left: 0 !important;
overflow-wrap: anywhere;
white-space: normal;
}
.step-name,
.step-agent,
.step-summary,
.hist-sha,
.hist-meta,
.report-box,
.ea-box,
code {
overflow-wrap: anywhere;
word-break: break-word;
}
.step-summary {
white-space: normal;
}
.hist-item > div:first-child {
align-items: flex-start;
flex-direction: column;
gap: 4px;
}
.tabs {
flex-wrap: wrap;
}
.tab {
flex: 1 1 150px;
min-width: 0;
text-align: center;
overflow-wrap: anywhere;
}
.empty {
padding: 28px 16px;
overflow-wrap: anywhere;
}
#findingsTable table,
#findingsTable thead,
#findingsTable tbody,
#findingsTable tr,
#findingsTable td {
display: block;
width: 100%;
}
#findingsTable thead {
display: none;
}
#findingsTable tr {
border-bottom: 1px solid var(--border);
padding: 10px 12px;
}
#findingsTable tr:last-child {
border-bottom: 0;
}
#findingsTable td {
border-bottom: 0;
padding: 6px 0;
overflow-wrap: anywhere;
}
#findingsTable td::before {
color: var(--muted);
content: "";
display: block;
font-size: 11px;
font-weight: 700;
letter-spacing: .04em;
margin-bottom: 3px;
text-transform: uppercase;
}
#findingsTable td:nth-child(1)::before { content: "嚴重度"; }
#findingsTable td:nth-child(2)::before { content: "類型"; }
#findingsTable td:nth-child(3)::before { content: "檔案 / 位置"; }
#findingsTable td:nth-child(4)::before { content: "問題說明"; }
#findingsTable td:nth-child(5)::before { content: "修復建議"; }
}
</style>
{% endblock %}
{% block content %}
<div class="code-review-page">
<!-- Top Bar -->
<div class="topbar">
<span>🔍</span>
<h1>部署守門與程式碼審查</h1>
<span class="badge">EwoooC · 業績流程上線前檢查</span>
<div id="liveDot" class="live-dot idle" title="流程狀態"></div>
</div>
<div class="layout">
<!-- ── Sidebar ─────────────────────────────────────────────────── -->
<div class="sidebar">
<!-- Review Steps -->
<div class="card">
<div class="card-header">審查流程
<span id="pipelineId" style="font-size:11px;color:var(--muted);margin-left:auto;"></span>
</div>
<div class="card-body">
<div class="pipeline" id="stepsContainer">
<div class="step" id="step-1"><div class="step-num">1</div><div class="step-info"><div class="step-name">讀取變更檔案</div><div class="step-agent">系統</div></div></div>
<div class="step" id="step-2"><div class="step-num">2</div><div class="step-info"><div class="step-name">程式碼掃描</div><div class="step-agent">靜態規則與風險掃描</div></div></div>
<div class="step" id="step-3"><div class="step-num">3</div><div class="step-info"><div class="step-name">架構品質評估</div><div class="step-agent">AI 延伸分析</div></div></div>
<div class="step" id="step-4"><div class="step-num">4</div><div class="step-info"><div class="step-name">上線決策</div><div class="step-agent">風險優先級判斷</div></div></div>
<div class="step" id="step-5"><div class="step-num">5</div><div class="step-info"><div class="step-name">修復派工</div><div class="step-agent">自動修復流程</div></div></div>
</div>
</div>
</div>
<!-- Severity Summary -->
<div class="card">
<div class="card-header">風險等級分佈</div>
<div class="card-body">
<div class="sev-grid">
<div class="sev-cell sev-critical"><div class="num" id="cnt-critical"></div><div class="lbl">🔴 最高</div></div>
<div class="sev-cell sev-high"><div class="num" id="cnt-high"></div><div class="lbl">🟠 高</div></div>
<div class="sev-cell sev-medium"><div class="num" id="cnt-medium"></div><div class="lbl">🟡 中</div></div>
<div class="sev-cell sev-low"><div class="num" id="cnt-low"></div><div class="lbl">🟢 低</div></div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">上線證據</div>
<div class="card-body" id="commitInfo" style="font-size:13px;color:var(--muted);line-height:1.8;">
<span style="color:var(--muted)">等待下次上線檢查...</span>
</div>
</div>
<!-- History -->
<div class="card">
<div class="card-header">🕐 歷史記錄</div>
<div class="card-body" style="padding:8px;" id="historyList">
<div class="empty"><div class="icon">📋</div><div>載入中...</div></div>
</div>
</div>
</div>
<!-- ── Main Content ─────────────────────────────────────────────── -->
<div class="main">
<!-- Status Bar -->
<div id="statusBar"></div>
<!-- Tabs -->
<div class="tabs">
<div class="tab active" onclick="switchTab('findings')">⚠️ 問題清單</div>
<div class="tab" onclick="switchTab('openclaw')">💡 架構評估</div>
<div class="tab" onclick="switchTab('ea')">🤖 上線決策</div>
</div>
<!-- Tab: Findings -->
<div class="tab-pane active" id="tab-findings">
<div class="card">
<div class="card-header">⚠️ 問題清單
<span id="findingsCount" style="font-size:11px;color:var(--muted);margin-left:auto"></span>
</div>
<div class="card-body" style="padding:0">
<div id="findingsTable">
<div class="empty"><div class="icon">🔍</div><div>等待程式碼審查完成...</div></div>
</div>
</div>
</div>
</div>
<!-- Tab: Architecture Review -->
<div class="tab-pane" id="tab-openclaw">
<div class="card">
<div class="card-header">💡 架構品質評估</div>
<div class="card-body">
<div class="report-box" id="openclawReport">
<span style="color:var(--muted)">等待架構分析完成...</span>
</div>
</div>
</div>
</div>
<!-- Tab: Release Decision -->
<div class="tab-pane" id="tab-ea">
<div class="card">
<div class="card-header">🤖 上線決策協調結果</div>
<div class="card-body">
<div id="eaDecision">
<span style="color:var(--muted)">等待上線決策...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const SEV_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
const SEV_LABEL = { CRITICAL: '最高', HIGH: '高', MEDIUM: '中', LOW: '低' };
const PRIORITY_LABEL = { critical: '最高', high: '高', medium: '中', low: '低' };
const DEPLOY_TYPE_LABEL = { sync: '同步部署', rebuild: '重建部署', manual: '手動觸發' };
let _polling = null;
let _lastPipelineId = null;
// ── Tab switching ──────────────────────────────────────────────────
function switchTab(name) {
document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', ['findings','openclaw','ea'][i] === name));
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
}
// ── Review step rendering ─────────────────────────────────────────
function renderSteps(steps, currentStep) {
for (let i = 1; i <= 5; i++) {
const el = document.getElementById('step-' + i);
if (!el) continue;
const info = steps.find(s => s.step === i);
el.className = 'step';
const numEl = el.querySelector('.step-num');
const summEl = el.querySelector('.step-summary') || (() => {
const d = document.createElement('div');
d.className = 'step-summary';
el.querySelector('.step-info').appendChild(d);
return d;
})();
if (info) {
if (info.status === 'ok') { el.classList.add('ok'); numEl.textContent = '✓'; }
else if (info.status === 'error') { el.classList.add('error'); numEl.textContent = '✗'; }
else if (info.status === 'running') { el.classList.add('running'); numEl.innerHTML = '<span class="spinner">⟳</span>'; }
summEl.textContent = info.summary || '';
} else if (i === currentStep) {
el.classList.add('running');
numEl.innerHTML = '<span class="spinner">⟳</span>';
} else {
numEl.textContent = i;
}
}
}
// ── Severity counters ─────────────────────────────────────────────
function renderSeverity(sev) {
['critical','high','medium','low'].forEach(k => {
const el = document.getElementById('cnt-' + k);
if (el) el.textContent = (sev && sev[k] !== undefined) ? sev[k] : '0';
});
}
// ── Findings table ────────────────────────────────────────────────
function renderFindings(findings) {
const el = document.getElementById('findingsTable');
document.getElementById('findingsCount').textContent = findings.length ? `${findings.length}` : '';
if (!findings.length) {
el.innerHTML = '<div class="empty"><div class="icon">✅</div><div>無發現問題</div></div>';
return;
}
const sorted = [...findings].sort((a,b) => (SEV_ORDER[a.severity]||3) - (SEV_ORDER[b.severity]||3));
el.innerHTML = `<table>
<thead><tr><th>嚴重度</th><th>類型</th><th>檔案 / 位置</th><th>問題說明</th><th>修復建議</th></tr></thead>
<tbody>${sorted.map(f => `
<tr>
<td><span class="badge-sev badge-${f.severity}">${SEV_LABEL[f.severity] || f.severity || '?'}</span></td>
<td><span class="badge-type">${f.type||'?'}</span></td>
<td><code>${(f.file||'').split('/').pop()}</code><br><span style="font-size:11px;color:var(--muted)">${f.line_hint||''}</span></td>
<td>${escHtml(f.description||'')}</td>
<td style="color:var(--muted)">${escHtml(f.suggestion||'')}</td>
</tr>`).join('')}
</tbody></table>`;
}
// ── Architecture report ───────────────────────────────────────────
function renderArchitectureReport(html) {
document.getElementById('openclawReport').innerHTML = html || '<span style="color:var(--muted)">(未取得)</span>';
}
// ── EA Decision ───────────────────────────────────────────────────
function renderEA(ea, autoFix) {
if (!ea || !ea.priority) {
document.getElementById('eaDecision').innerHTML = '<span style="color:var(--muted)">等待上線決策...</span>';
return;
}
const priColors = { critical: 'var(--red)', high: 'var(--orange)', medium: 'var(--yellow)', low: 'var(--green)' };
const col = priColors[ea.priority] || 'var(--blue)';
const priorityLabel = PRIORITY_LABEL[ea.priority] || ea.priority || '待確認';
const fixFiles = (ea.fix_files||[]).map(f=>`<code>${f}</code>`).join(' ');
document.getElementById('eaDecision').innerHTML = `
<div class="ea-box ${ea.priority}">
<div style="display:flex;align-items:center;gap:10px">
<div class="ea-priority" style="color:${col}">${priorityLabel}</div>
<div>${autoFix ? '🔧 <b>自動修復已觸發</b>' : (ea.human_review_needed ? '👁 <b>需人工審查</b>' : '✅ <b>無需修復</b>')}</div>
</div>
<div class="ea-reasoning">${escHtml(ea.reasoning||'')}</div>
${fixFiles ? `<div class="ea-fix">修復範圍:${fixFiles}</div>` : ''}
</div>`;
}
// ── 上線證據 ─────────────────────────────────────────────────────
function renderCommitInfo(state) {
const el = document.getElementById('commitInfo');
if (!state.commit_sha) return;
const changedCount = (state.changed_files||[]).length;
el.innerHTML = `
<div><b>狀態</b> 已收到上線檢查資料</div>
<div><b>模式</b> ${DEPLOY_TYPE_LABEL[state.deploy_type] || state.deploy_type || '同步部署'}</div>
<div><b>變更</b> ${changedCount} 項待檢查</div>`;
}
// ── Status bar ────────────────────────────────────────────────────
function renderStatusBar(state) {
const el = document.getElementById('statusBar');
const dot = document.getElementById('liveDot');
if (!state.status) { el.style.display='none'; return; }
el.style.display = 'block';
el.className = state.status;
const msgs = {
running: `⟳ <b>流程執行中</b> — 第 ${state.current_step}/5 步`,
completed: `✅ <b>程式碼審查完成</b> — ${state.message||''}`,
error: `❌ <b>流程需確認</b> — 請查看風險清單`,
skipped: `⏭ <b>已略過</b> — ${escHtml(state.message||'')}`,
};
el.innerHTML = msgs[state.status] || '';
dot.className = 'live-dot' + (state.status === 'running' ? '' : ' idle');
}
// ── History ───────────────────────────────────────────────────────
let _historyData = [];
function renderHistory(items) {
_historyData = items;
const el = document.getElementById('historyList');
if (!items.length) { el.innerHTML = '<div class="empty"><div>尚無歷史記錄</div></div>'; return; }
el.innerHTML = items.map((h, idx) => {
const sev = h.severity_summary || {};
return `<div class="hist-item" onclick="loadHistoryItem(${idx})" data-idx="${idx}">
<div style="display:flex;justify-content:space-between">
<span class="hist-sha">上線檢查</span>
<span style="font-size:11px;color:var(--muted)">${h.created_at.slice(0,16).replace('T',' ')}</span>
</div>
<div class="hist-meta">${(h.changed_files||[]).length} 項變更${h.auto_fix?' • 已完成修復流程':''}</div>
<div class="hist-sev">
${sev.critical?`<span style="background:rgba(248,81,73,.2);color:var(--red)">🔴 ${sev.critical}</span>`:''}
${sev.high?`<span style="background:rgba(210,153,34,.2);color:var(--orange)">🟠 ${sev.high}</span>`:''}
${sev.medium?`<span style="background:rgba(227,179,65,.2);color:var(--yellow)">🟡 ${sev.medium}</span>`:''}
${sev.low?`<span style="background:rgba(63,185,80,.2);color:var(--green)">🟢 ${sev.low}</span>`:''}
${!h.total_issues?`<span style="color:var(--green)">✅ 無問題</span>`:''}
</div>
</div>`;
}).join('');
}
function loadHistoryItem(idx) {
const h = _historyData[idx];
if (!h) return;
document.querySelectorAll('.hist-item').forEach((el, i) => {
el.style.borderColor = i === idx ? 'var(--blue)' : '';
});
renderSeverity(h.severity_summary);
const changedCount = (h.changed_files||[]).length;
document.getElementById('commitInfo').innerHTML = `
<div><b>狀態</b> 已完成上線檢查</div>
<div><b>時間</b> ${h.created_at.slice(0,16).replace('T',' ')}</div>
<div><b>變更</b> ${changedCount} 項已檢查</div>`;
document.getElementById('pipelineId').textContent = '最近一次';
const sBar = document.getElementById('statusBar');
sBar.style.display = 'block';
sBar.className = 'completed';
sBar.innerHTML = `✅ <b>歷史記錄</b> — 已完成上線檢查${h.auto_fix ? ',並完成修復流程' : ''}`;
renderFindings(h.findings || []);
renderArchitectureReport(h.openclaw_report || '');
renderEA(h.ea_decision || {}, h.auto_fix || false);
for (let i = 1; i <= 5; i++) {
const el = document.getElementById('step-' + i);
if (el) { el.className = 'step ok'; el.querySelector('.step-num').textContent = '✓'; }
}
}
// ── Main polling loop ─────────────────────────────────────────────
async function poll() {
try {
const r = await fetch('/code-review/api/status');
const state = await r.json();
if (state.pipeline_id !== _lastPipelineId && state.pipeline_id) {
_lastPipelineId = state.pipeline_id;
}
renderStatusBar(state);
renderSteps(state.steps||[], state.current_step||0);
renderSeverity(state.severity_summary);
renderFindings(state.findings||[]);
renderArchitectureReport(state.openclaw_report);
renderEA(state.ea_decision, state.auto_fix_triggered);
renderCommitInfo(state);
document.getElementById('pipelineId').textContent = state.pipeline_id ? '執行中' : '';
// 每 3s 輪詢running/ 30sidle
const interval = state.status === 'running' ? 3000 : 30000;
_polling = setTimeout(poll, interval);
} catch(e) {
_polling = setTimeout(poll, 10000);
}
}
async function loadHistory() {
try {
const r = await fetch('/code-review/api/history?limit=15');
const items = await r.json();
renderHistory(items);
// 無 active pipeline 且尚未顯示任何 pipeline 內容時,自動展示最新一筆
if (items.length && !_lastPipelineId) {
loadHistoryItem(0);
}
} catch(e) {
document.getElementById('historyList').innerHTML = '<div class="empty"><div>載入失敗</div></div>';
}
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ── Init ──────────────────────────────────────────────────────────
poll();
loadHistory();
setInterval(loadHistory, 60000);
</script>
{% endblock %}