log-center/web/src/pages/BugDetail.tsx
zyc 625e53dc44
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m16s
feat(project-mgmt): 项目管理 + 失败原因追踪 + 前端展示
- 新增 Project 模型(repo_url, local_path, name, description)
- 项目 CRUD API(GET/PUT /api/v1/projects)
- 日志上报自动 upsert Project 记录
- ErrorLog 增加 failure_reason 字段
- update_task_status / create_repair_report 写入失败原因
- Repair Agent 优先从 API 获取项目配置,回退 .env
- 新增 Web 端「项目管理」页面(表格 + 行内编辑)
- BugList/BugDetail/RepairList 展示失败原因
- 更新接入指南文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 11:18:27 +08:00

296 lines
13 KiB
TypeScript
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.

import { useState, useEffect } from 'react';
import { useParams, Link, useLocation } from 'react-router-dom';
import { ArrowLeft, Play, Loader2, FileCode, GitCommit, History, AlertTriangle } from 'lucide-react';
import { getBugDetail, triggerRepair, getRepairReportsByBug, type ErrorLog, type RepairReport } from '../api';
const SOURCE_LABELS: Record<string, string> = {
runtime: '运行时',
cicd: 'CI/CD',
deployment: '部署',
};
const STATUS_LABELS: Record<string, string> = {
NEW: '新发现',
VERIFYING: '验证中',
CANNOT_REPRODUCE: '无法复现',
PENDING_FIX: '待修复',
FIXING: '修复中',
FIXED: '已修复',
VERIFIED: '已验证',
DEPLOYED: '已部署',
FIX_FAILED: '修复失败',
};
export default function BugDetail() {
const { id } = useParams<{ id: string }>();
const location = useLocation();
const [bug, setBug] = useState<ErrorLog | null>(null);
const [loading, setLoading] = useState(true);
const [repairing, setRepairing] = useState(false);
const [repairMessage, setRepairMessage] = useState('');
const [repairHistory, setRepairHistory] = useState<RepairReport[]>([]);
const backSearch = location.state?.fromSearch || '';
useEffect(() => {
const fetchBug = async () => {
if (!id) return;
try {
const response = await getBugDetail(parseInt(id));
setBug(response.data);
} catch (error) {
console.error('Failed to fetch bug:', error);
} finally {
setLoading(false);
}
};
fetchBug();
}, [id]);
useEffect(() => {
if (id) {
getRepairReportsByBug(parseInt(id)).then(res => {
setRepairHistory(res.data.items);
}).catch(console.error);
}
}, [id]);
const handleTriggerRepair = async () => {
if (!bug) return;
setRepairing(true);
setRepairMessage('');
try {
await triggerRepair(bug.id);
setBug({ ...bug, status: 'PENDING_FIX' });
setRepairMessage('已成功触发修复');
} catch (error) {
console.error('Failed to trigger repair:', error);
setRepairMessage('触发修复失败');
} finally {
setRepairing(false);
}
};
if (loading) {
return (
<div className="loading">
<div className="spinner"></div>
</div>
);
}
if (!bug) {
return <div className="loading"></div>;
}
const isRuntime = !bug.source || bug.source === 'runtime';
const canTriggerRepair = ['NEW', 'FIX_FAILED'].includes(bug.status) && isRuntime;
return (
<div>
<Link to={`/bugs${backSearch ? `?${backSearch}` : ''}`} className="back-link">
<ArrowLeft size={14} />
</Link>
<div className="detail-card">
<div className="detail-header">
<div>
<h2 className="detail-title" style={{ color: 'var(--error)' }}>
{bug.error_type}: {bug.error_message}
</h2>
<div className="detail-meta">
<span>{bug.project_id}</span>
<span><span className={`source-badge source-${bug.source || 'runtime'}`}>{SOURCE_LABELS[bug.source] || '运行时'}</span></span>
<span>{bug.environment}</span>
<span>{bug.level}</span>
</div>
</div>
<span className={`status-badge status-${bug.status}`}>
{STATUS_LABELS[bug.status] || bug.status}
</span>
</div>
{bug.file_path && (
<div className="detail-section">
<div className="detail-section-title"></div>
<div className="detail-section-value">
<FileCode size={14} style={{ display: 'inline', verticalAlign: 'middle', marginRight: '6px' }} />
{bug.file_path} : {bug.line_number}
</div>
</div>
)}
{bug.source === 'cicd' && bug.context?.workflow_name && (
<div className="detail-section">
<div className="detail-section-title">CI/CD </div>
<div className="detail-section-value">
{bug.context.workflow_name} / {bug.context.job_name} / {bug.context.step_name}
{bug.context.branch && <><br />{bug.context.branch}</>}
{bug.context.run_url && (
<><br /><a href={bug.context.run_url} target="_blank" rel="noopener noreferrer"> CI </a></>
)}
</div>
</div>
)}
{bug.source === 'deployment' && bug.context?.pod_name && (
<div className="detail-section">
<div className="detail-section-title"></div>
<div className="detail-section-value">
{bug.context.namespace} | Pod{bug.context.pod_name}
<br />
{bug.context.container_name} | {bug.context.restart_count}
{bug.context.node_name && <><br />{bug.context.node_name}</>}
</div>
</div>
)}
{bug.commit_hash && (
<div className="detail-section">
<div className="detail-section-title">Git </div>
<div className="detail-section-value">
<GitCommit size={14} style={{ display: 'inline', verticalAlign: 'middle', marginRight: '6px' }} />
{bug.commit_hash}
{bug.version && ` | v${bug.version}`}
</div>
</div>
)}
<div className="detail-section">
<div className="detail-section-title"></div>
<pre className="code-block error">
{typeof bug.stack_trace === 'string'
? bug.stack_trace
: JSON.stringify(bug.stack_trace, null, 2)}
</pre>
</div>
{bug.context && Object.keys(bug.context).length > 0 && (
<div className="detail-section">
<div className="detail-section-title"></div>
<pre className="code-block accent">
{JSON.stringify(bug.context, null, 2)}
</pre>
</div>
)}
<div className="actions-bar">
<button
className="trigger-repair-btn"
onClick={handleTriggerRepair}
disabled={!canTriggerRepair || repairing}
>
{repairing ? (
<Loader2 size={14} className="spinner" />
) : (
<Play size={14} />
)}
{repairing ? '触发中...' : '触发修复'}
</button>
{repairMessage && (
<span style={{
fontSize: '13px',
color: repairMessage.includes('成功') ? 'var(--success)' : 'var(--error)'
}}>
{repairMessage}
</span>
)}
{!canTriggerRepair && !repairing && (
<span style={{ fontSize: '13px', color: 'var(--text-tertiary)' }}>
{!isRuntime
? 'CI/CD 和部署错误暂不支持自动修复'
: '仅"新发现"或"修复失败"状态的缺陷可触发修复'}
</span>
)}
</div>
</div>
{bug.failure_reason && (
<div className="detail-card" style={{ borderLeft: '3px solid var(--error)' }}>
<div className="detail-section-title" style={{ marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--error)' }}>
<AlertTriangle size={14} />
</div>
<pre className="code-block error">{bug.failure_reason}</pre>
</div>
)}
<div className="detail-card">
<div className="detail-section-title" style={{ marginBottom: '12px' }}></div>
<table className="meta-table">
<tbody>
<tr>
<td className="meta-label"></td>
<td>{bug.id}</td>
</tr>
<tr>
<td className="meta-label"></td>
<td className="cell-mono">{bug.fingerprint}</td>
</tr>
<tr>
<td className="meta-label"></td>
<td>{bug.retry_count}</td>
</tr>
<tr>
<td className="meta-label"></td>
<td>{new Date(bug.timestamp).toLocaleString()}</td>
</tr>
</tbody>
</table>
</div>
{repairHistory.length > 0 && (
<div className="detail-card">
<div className="detail-section-title" style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
<History size={14} />
({repairHistory.length} )
</div>
<div className="table-container">
<table>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{repairHistory.map(report => (
<tr key={report.id}>
<td> {report.repair_round} </td>
<td>
<span className={`status-badge status-${report.status}`}>
{STATUS_LABELS[report.status] || report.status}
</span>
</td>
<td>
<span className={report.test_passed ? 'test-pass' : 'test-fail'}>
{report.test_passed ? '通过' : '失败'}
</span>
</td>
<td style={{ maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{report.failure_reason || '-'}
</td>
<td className="cell-secondary">
{new Date(report.created_at).toLocaleString()}
</td>
<td>
<Link to={`/repairs/${report.id}`} className="btn-link">
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}