Files
ewoooc/templates/code_review.html
OoO 42de2e4d3f
All checks were successful
CD Pipeline / deploy (push) Successful in 57s
擴大全站響應式守門範圍
2026-05-13 23:17:57 +08:00

519 lines
26 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.
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EwoooC — AI Code Review Dashboard</title>
<style>
:root {
--bg: #0d1117;
--panel: #161b22;
--border: #30363d;
--text: #e6edf3;
--muted: #8b949e;
--red: #f85149;
--orange: #d29922;
--yellow: #e3b341;
--green: #3fb950;
--blue: #58a6ff;
--purple: #bc8cff;
--accent: #e94560;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, 'Segoe UI', sans-serif; font-size: 14px; }
/* ── Layout ─────────────────────────────────────────── */
.topbar { background: var(--panel); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; align-items: center; gap: 12px; }
.topbar h1 { font-size: 18px; font-weight: 700; color: var(--accent); }
.topbar .badge { font-size: 11px; padding: 2px 8px; border-radius: 12px; background: #21262d; color: var(--muted); }
.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: 340px 1fr; gap: 0; height: calc(100vh - 49px); overflow: hidden; }
.sidebar { background: var(--panel); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px; }
.main { overflow-y: auto; padding: 16px; }
/* ── Card ───────────────────────────────────────────── */
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
.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: 6px; border: 1px solid var(--border); background: #0d1117; transition: border-color .3s; }
.step.running { border-color: var(--blue); background: rgba(88,166,255,.07); }
.step.ok { border-color: var(--green); background: rgba(63,185,80,.06); }
.step.error { border-color: var(--red); background: rgba(248,81,73,.07); }
.step-num { width: 22px; height: 22px; border-radius: 50%; background: #21262d; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.step.ok .step-num { background: var(--green); color: #000; }
.step.running .step-num { background: var(--blue); color: #000; }
.step.error .step-num { background: var(--red); color: #fff; }
.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 #21262d; vertical-align: top; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255,255,255,.02); }
.badge-sev { display: inline-block; padding: 2px 7px; border-radius: 10px; font-size: 11px; font-weight: 700; }
.badge-CRITICAL { background: rgba(248,81,73,.2); color: var(--red); }
.badge-HIGH { background: rgba(210,153,34,.2); color: var(--orange); }
.badge-MEDIUM { background: rgba(227,179,65,.2); color: var(--yellow); }
.badge-LOW { background: rgba(63,185,80,.2); color: var(--green); }
.badge-type { display: inline-block; padding: 1px 6px; border-radius: 8px; font-size: 10px; background: #21262d; color: var(--muted); }
code { background: #21262d; padding: 1px 5px; border-radius: 4px; font-size: 12px; color: var(--blue); }
/* ── OpenClaw Report ─────────────────────────────────── */
.report-box { background: #0d1117; border: 1px solid var(--border); border-radius: 6px; 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: rgba(88,166,255,.06); }
.ea-box.critical { border-left-color: var(--red); background: rgba(248,81,73,.07); }
.ea-box.high { border-left-color: var(--orange); background: rgba(210,153,34,.07); }
.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: rgba(88,166,255,.1); border: 1px solid var(--blue); }
#statusBar.completed { background: rgba(63,185,80,.1); border: 1px solid var(--green); }
#statusBar.error { background: rgba(248,81,73,.1); border: 1px solid var(--red); }
#statusBar.skipped { background: rgba(139,148,158,.1); 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: #21262d; color: var(--text); border-color: var(--border); }
.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 {
overflow: visible;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid var(--border);
}
.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;
}
}
</style>
</head>
<body>
<!-- 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>
<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/ 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>
</body>
</html>