Files
ewoooc/templates/code_review.html
ogt aadbce73e5
All checks were successful
CD Pipeline / deploy (push) Successful in 1m12s
fix: sanitize observability and review UI copy
2026-06-25 15:18:15 +08:00

630 lines
29 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); }
/* ── OpenClaw 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;font-family:monospace"></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>
<!-- Commit Info -->
<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 renderOpenClaw(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>`;
}
// ── Commit info ───────────────────────────────────────────────────
function renderCommitInfo(state) {
const el = document.getElementById('commitInfo');
if (!state.commit_sha) return;
const files = (state.changed_files||[]).slice(0,5).map(f=>`<code>${f.split('/').pop()}</code>`).join(' ');
const more = (state.changed_files||[]).length > 5 ? `<span style="color:var(--muted)">+${state.changed_files.length-5}</span>` : '';
el.innerHTML = `
<div><b>提交</b> <code>${state.commit_sha.slice(0,8)}</code></div>
<div><b>分支</b> <code>${state.branch||'?'}</code></div>
<div><b>模式</b> ${DEPLOY_TYPE_LABEL[state.deploy_type] || state.deploy_type || '同步部署'}</div>
<div><b>變更</b> ${files} ${more}</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> — ${escHtml(state.message||'')}`,
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">${h.commit_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.branch}${(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 files = (h.changed_files||[]).slice(0,5).map(f=>`<code>${f.split('/').pop()}</code>`).join(' ');
const more = (h.changed_files||[]).length > 5 ? `<span style="color:var(--muted)">+${h.changed_files.length-5}</span>` : '';
document.getElementById('commitInfo').innerHTML = `
<div><b>提交</b> <code>${h.commit_sha}</code></div>
<div><b>分支</b> <code>${h.branch||'?'}</code></div>
<div><b>時間</b> ${h.created_at.slice(0,16).replace('T',' ')}</div>
<div><b>變更</b> ${files} ${more}</div>`;
document.getElementById('pipelineId').textContent = (h.pipeline_id||'').slice(-14);
const sBar = document.getElementById('statusBar');
sBar.style.display = 'block';
sBar.className = 'completed';
sBar.innerHTML = `✅ <b>歷史記錄</b> — 提交 ${h.commit_sha}${h.auto_fix ? ' 🔧 已自動修復' : ''}`;
renderFindings(h.findings || []);
renderOpenClaw(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||[]);
renderOpenClaw(state.openclaw_report);
renderEA(state.ea_decision, state.auto_fix_triggered);
renderCommitInfo(state);
document.getElementById('pipelineId').textContent = (state.pipeline_id||'').slice(-14);
// 每 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 %}