626 lines
29 KiB
HTML
626 lines
29 KiB
HTML
{% extends "ewoooc_base.html" %}
|
||
|
||
{% block title %}AI Code Review - 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>AI Code Review</h1>
|
||
<span class="badge">EwoooC · Post-Deploy Pipeline</span>
|
||
<div id="liveDot" class="live-dot idle" title="Pipeline 狀態"></div>
|
||
</div>
|
||
|
||
<div class="layout">
|
||
|
||
<!-- ── Sidebar ─────────────────────────────────────────────────── -->
|
||
<div class="sidebar">
|
||
|
||
<!-- Pipeline Steps -->
|
||
<div class="card">
|
||
<div class="card-header">🤖 Pipeline 進度
|
||
<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">system</div></div></div>
|
||
<div class="step" id="step-2"><div class="step-num">2</div><div class="step-info"><div class="step-name">Hermes 程式碼掃描</div><div class="step-agent">Hermes · hermes3:latest</div></div></div>
|
||
<div class="step" id="step-3"><div class="step-num">3</div><div class="step-info"><div class="step-name">OpenClaw 架構評估</div><div class="step-agent">OpenClaw · Gemini 2.5</div></div></div>
|
||
<div class="step" id="step-4"><div class="step-num">4</div><div class="step-info"><div class="step-name">Elephant Alpha 決策</div><div class="step-agent">Elephant Alpha · 100B</div></div></div>
|
||
<div class="step" id="step-5"><div class="step-num">5</div><div class="step-info"><div class="step-name">NemoTron 行動派遣</div><div class="step-agent">NemoTron · Dispatcher</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">🔴 CRITICAL</div></div>
|
||
<div class="sev-cell sev-high"><div class="num" id="cnt-high">—</div><div class="lbl">🟠 HIGH</div></div>
|
||
<div class="sev-cell sev-medium"><div class="num" id="cnt-medium">—</div><div class="lbl">🟡 MEDIUM</div></div>
|
||
<div class="sev-cell sev-low"><div class="num" id="cnt-low">—</div><div class="lbl">🟢 LOW</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')">💡 OpenClaw 評估</div>
|
||
<div class="tab" onclick="switchTab('ea')">🤖 Elephant Alpha 決策</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>等待 Code Review 完成...</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: OpenClaw -->
|
||
<div class="tab-pane" id="tab-openclaw">
|
||
<div class="card">
|
||
<div class="card-header">💡 OpenClaw 架構品質評估</div>
|
||
<div class="card-body">
|
||
<div class="report-box" id="openclawReport">
|
||
<span style="color:var(--muted)">等待 OpenClaw 分析完成...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: EA Decision -->
|
||
<div class="tab-pane" id="tab-ea">
|
||
<div class="card">
|
||
<div class="card-header">🤖 Elephant Alpha 決策協調結果</div>
|
||
<div class="card-body">
|
||
<div id="eaDecision">
|
||
<span style="color:var(--muted)">等待 Elephant Alpha 決策...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
const SEV_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
||
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');
|
||
}
|
||
|
||
// ── Pipeline 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}">${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>`;
|
||
}
|
||
|
||
// ── OpenClaw 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 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}">${ea.priority.toUpperCase()}</div>
|
||
<div>${autoFix ? '🔧 <b>自動修復已觸發(AiderHeal)</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>Commit</b> <code>${state.commit_sha.slice(0,8)}</code></div>
|
||
<div><b>Branch</b> <code>${state.branch||'?'}</code></div>
|
||
<div><b>模式</b> ${state.deploy_type||'sync'}</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>Pipeline 執行中</b> — Step ${state.current_step}/5`,
|
||
completed: `✅ <b>Code Review 完成</b> — ${state.message||''}`,
|
||
error: `❌ <b>Pipeline 失敗</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>Commit</b> <code>${h.commit_sha}</code></div>
|
||
<div><b>Branch</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> — Commit ${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)/ 30s(idle)
|
||
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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
// ── Init ──────────────────────────────────────────────────────────
|
||
poll();
|
||
loadHistory();
|
||
setInterval(loadHistory, 60000);
|
||
</script>
|
||
{% endblock %}
|