add UI
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m42s

This commit is contained in:
zyc 2026-02-10 14:55:28 +08:00
parent 2d03f01ecc
commit 20a3b0b374
10 changed files with 914 additions and 409 deletions

10
web/package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.13.4",
"lucide-react": "^0.563.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0"
@ -2872,6 +2873,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.563.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
"integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

View File

@ -11,6 +11,7 @@
},
"dependencies": {
"axios": "^1.13.4",
"lucide-react": "^0.563.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0"

View File

@ -1,4 +1,5 @@
import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
import { LayoutDashboard, Bug, Wrench, Shield } from 'lucide-react';
import Dashboard from './pages/Dashboard';
import BugList from './pages/BugList';
import BugDetail from './pages/BugDetail';
@ -12,7 +13,10 @@ function App() {
<div className="app">
<aside className="sidebar">
<div className="logo">
🛡 <span>Log Center</span>
<div className="logo-icon">
<Shield size={16} />
</div>
Log Center
</div>
<nav>
<ul className="nav-menu">
@ -22,7 +26,8 @@ function App() {
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
end
>
📊 Dashboard
<LayoutDashboard size={16} />
Dashboard
</NavLink>
</li>
<li className="nav-item">
@ -30,7 +35,8 @@ function App() {
to="/bugs"
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
>
🐛 Bug List
<Bug size={16} />
Bug List
</NavLink>
</li>
<li className="nav-item">
@ -38,7 +44,8 @@ function App() {
to="/repairs"
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
>
🔧 Repair Reports
<Wrench size={16} />
Repair Reports
</NavLink>
</li>
</ul>

View File

@ -70,8 +70,11 @@ export const getBugDetail = (id: number) => api.get<ErrorLog>(`/api/v1/bugs/${id
export const getProjects = () => api.get<{ projects: string[] }>('/api/v1/projects');
export const updateTaskStatus = (taskId: number, status: string) =>
api.put(`/api/v1/tasks/${taskId}/status`, { status });
export const updateTaskStatus = (taskId: number, status: string, message?: string) =>
api.put(`/api/v1/tasks/${taskId}/status`, { status, message });
export const triggerRepair = (bugId: number) =>
updateTaskStatus(bugId, 'PENDING_FIX', 'Triggered from UI');
export const getRepairReports = (params: {
page?: number;
@ -82,4 +85,3 @@ export const getRepairReports = (params: {
export const getRepairReportDetail = (id: number) => api.get<RepairReport>(`/api/v1/repair/reports/${id}`);
export default api;

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,16 @@
import { useState, useEffect } from 'react';
import { useParams, Link, useLocation } from 'react-router-dom';
import { getBugDetail, type ErrorLog } from '../api';
import { ArrowLeft, Play, Loader2, FileCode, GitCommit } from 'lucide-react';
import { getBugDetail, triggerRepair, type ErrorLog } from '../api';
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('');
// Preserve search params from the list page for back navigation
const backSearch = location.state?.fromSearch || '';
useEffect(() => {
@ -26,6 +28,22 @@ export default function BugDetail() {
fetchBug();
}, [id]);
const handleTriggerRepair = async () => {
if (!bug) return;
setRepairing(true);
setRepairMessage('');
try {
await triggerRepair(bug.id);
setBug({ ...bug, status: 'PENDING_FIX' });
setRepairMessage('Repair triggered successfully');
} catch (error) {
console.error('Failed to trigger repair:', error);
setRepairMessage('Failed to trigger repair');
} finally {
setRepairing(false);
}
};
if (loading) {
return (
<div className="loading">
@ -38,10 +56,13 @@ export default function BugDetail() {
return <div className="loading">Bug not found</div>;
}
const canTriggerRepair = ['NEW', 'FIX_FAILED'].includes(bug.status);
return (
<div>
<Link to={`/bugs${backSearch ? `?${backSearch}` : ''}`} className="back-link">
Back to Bug List
<ArrowLeft size={14} />
Back to Bug List
</Link>
<div className="detail-card">
@ -52,33 +73,35 @@ export default function BugDetail() {
</h2>
<div className="detail-meta">
<span>Project: {bug.project_id}</span>
<span>Environment: {bug.environment}</span>
<span>Env: {bug.environment}</span>
<span>Level: {bug.level}</span>
</div>
</div>
<span className={`status-badge status-${bug.status}`}>{bug.status}</span>
</div>
<div style={{ marginBottom: '24px' }}>
<h4 style={{ marginBottom: '12px', color: 'var(--text-secondary)' }}>Location</h4>
<p style={{ fontFamily: 'monospace', fontSize: '14px' }}>
📁 {bug.file_path} : Line {bug.line_number}
</p>
<div className="detail-section">
<div className="detail-section-title">Location</div>
<div className="detail-section-value">
<FileCode size={14} style={{ display: 'inline', verticalAlign: 'middle', marginRight: '6px' }} />
{bug.file_path} : Line {bug.line_number}
</div>
</div>
{bug.commit_hash && (
<div style={{ marginBottom: '24px' }}>
<h4 style={{ marginBottom: '12px', color: 'var(--text-secondary)' }}>Git Info</h4>
<p style={{ fontFamily: 'monospace', fontSize: '14px' }}>
Commit: {bug.commit_hash}
{bug.version && ` | Version: ${bug.version}`}
</p>
<div className="detail-section">
<div className="detail-section-title">Git Info</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 style={{ marginBottom: '24px' }}>
<h4 style={{ marginBottom: '12px', color: 'var(--text-secondary)' }}>Stack Trace</h4>
<pre className="stack-trace">
<div className="detail-section">
<div className="detail-section-title">Stack Trace</div>
<pre className="code-block error">
{typeof bug.stack_trace === 'string'
? bug.stack_trace
: JSON.stringify(bug.stack_trace, null, 2)}
@ -86,33 +109,61 @@ export default function BugDetail() {
</div>
{bug.context && Object.keys(bug.context).length > 0 && (
<div>
<h4 style={{ marginBottom: '12px', color: 'var(--text-secondary)' }}>Context</h4>
<pre className="stack-trace" style={{ color: 'var(--accent)' }}>
<div className="detail-section">
<div className="detail-section-title">Context</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 ? 'Triggering...' : 'Trigger Repair'}
</button>
{repairMessage && (
<span style={{
fontSize: '13px',
color: repairMessage.includes('success') ? 'var(--success)' : 'var(--error)'
}}>
{repairMessage}
</span>
)}
{!canTriggerRepair && !repairing && (
<span style={{ fontSize: '13px', color: 'var(--text-tertiary)' }}>
Only NEW or FIX_FAILED bugs can be triggered
</span>
)}
</div>
</div>
<div className="detail-card">
<h4 style={{ marginBottom: '16px' }}>Metadata</h4>
<table>
<div className="detail-section-title" style={{ marginBottom: '12px' }}>Metadata</div>
<table className="meta-table">
<tbody>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Bug ID</td>
<td className="meta-label">Bug ID</td>
<td>{bug.id}</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Fingerprint</td>
<td style={{ fontFamily: 'monospace', fontSize: '13px' }}>{bug.fingerprint}</td>
<td className="meta-label">Fingerprint</td>
<td className="cell-mono">{bug.fingerprint}</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Retry Count</td>
<td className="meta-label">Retry Count</td>
<td>{bug.retry_count}</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Reported At</td>
<td className="meta-label">Reported At</td>
<td>{new Date(bug.timestamp).toLocaleString()}</td>
</tr>
</tbody>

View File

@ -15,7 +15,6 @@ export default function BugList() {
const [totalPages, setTotalPages] = useState(1);
const [projects, setProjects] = useState<string[]>([]);
// Read filters from URL params, default status to NEW
const currentProject = searchParams.get('project') || '';
const currentStatus = searchParams.get('status') ?? 'NEW';
const currentPage = parseInt(searchParams.get('page') || '1', 10);
@ -30,7 +29,6 @@ export default function BugList() {
next.delete(key);
}
}
// Reset page when changing filters
if ('project' in updates || 'status' in updates) {
next.delete('page');
}
@ -77,7 +75,6 @@ export default function BugList() {
<p className="page-subtitle">All reported errors and their current status</p>
</div>
{/* Project breadcrumb navigation */}
<div className="project-tabs">
<button
className={`project-tab ${currentProject === '' ? 'active' : ''}`}
@ -96,7 +93,6 @@ export default function BugList() {
))}
</div>
{/* Status filter tabs */}
<div className="status-tabs">
<button
className={`status-tab ${currentStatus === '' ? 'active' : ''}`}
@ -146,7 +142,6 @@ export default function BugList() {
<Link
to={`/bugs/${bug.id}`}
state={{ fromSearch: searchParams.toString() }}
style={{ color: 'var(--accent)' }}
>
#{bug.id}
</Link>
@ -159,8 +154,8 @@ export default function BugList() {
{bug.project_id}
</button>
</td>
<td style={{ color: 'var(--error)' }}>{bug.error_type}</td>
<td style={{ fontFamily: 'monospace', fontSize: '13px' }}>
<td className="cell-error">{bug.error_type}</td>
<td className="cell-mono">
{bug.file_path}:{bug.line_number}
</td>
<td>
@ -168,7 +163,7 @@ export default function BugList() {
{bug.status}
</span>
</td>
<td style={{ color: 'var(--text-secondary)', fontSize: '14px' }}>
<td className="cell-secondary">
{new Date(bug.timestamp).toLocaleString()}
</td>
</tr>
@ -184,7 +179,7 @@ export default function BugList() {
>
Previous
</button>
<span style={{ padding: '8px 16px', color: 'var(--text-secondary)' }}>
<span className="pagination-info">
Page {currentPage} of {totalPages}
</span>
<button

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { Bug, CalendarPlus, TrendingUp, AlertTriangle } from 'lucide-react';
import { getStats, type DashboardStats } from '../api';
export default function Dashboard() {
@ -40,18 +41,30 @@ export default function Dashboard() {
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon accent">
<Bug size={18} />
</div>
<div className="stat-label">Total Bugs</div>
<div className="stat-value accent">{stats.total_bugs}</div>
</div>
<div className="stat-card">
<div className="stat-label">Today's New Bugs</div>
<div className="stat-icon warning">
<CalendarPlus size={18} />
</div>
<div className="stat-label">Today's New</div>
<div className="stat-value warning">{stats.today_bugs}</div>
</div>
<div className="stat-card">
<div className="stat-icon success">
<TrendingUp size={18} />
</div>
<div className="stat-label">Fix Rate</div>
<div className="stat-value success">{stats.fix_rate}%</div>
</div>
<div className="stat-card">
<div className="stat-icon error">
<AlertTriangle size={18} />
</div>
<div className="stat-label">Pending Fix</div>
<div className="stat-value error">
{(stats.status_distribution['NEW'] || 0) +

View File

@ -1,21 +1,18 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ArrowLeft, Bot, FileCode, FlaskConical } from 'lucide-react';
import { getRepairReportDetail, type RepairReport } from '../api';
function RepairDetail() {
export default function RepairDetail() {
const { id } = useParams<{ id: string }>();
const [report, setReport] = useState<RepairReport | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (id) {
fetchDetail(parseInt(id));
}
}, [id]);
const fetchDetail = async (reportId: number) => {
const fetchDetail = async () => {
try {
const res = await getRepairReportDetail(reportId);
const res = await getRepairReportDetail(parseInt(id));
setReport(res.data);
} catch (err) {
console.error(err);
@ -23,65 +20,72 @@ function RepairDetail() {
setLoading(false);
}
};
fetchDetail();
}
}, [id]);
if (loading) return <div className="loading">Loading...</div>;
if (!report) return <div className="error">Report not found</div>;
if (loading) {
return (
<div className="loading">
<div className="spinner"></div>
</div>
);
}
if (!report) {
return <div className="loading">Report not found</div>;
}
return (
<div className="bug-detail-page">
<div className="header">
<div className="breadcrumb">
<Link to="/repairs">Repair Reports</Link> / #{report.id}
</div>
<div>
<Link to="/repairs" className="back-link">
<ArrowLeft size={14} />
Back to Repair Reports
</Link>
<div style={{ marginBottom: '20px' }}>
<div className="title-row">
<h1>Repair Report #{report.id}</h1>
<span className={`status-badge ${report.status}`}>{report.status}</span>
<h1 className="page-title">Repair Report #{report.id}</h1>
<span className={`status-badge status-${report.status}`}>{report.status}</span>
</div>
</div>
<div className="detail-grid" style={{ gridTemplateColumns: '1fr' }}>
<div className="card">
<h2>Basic Info</h2>
<div className="info-row">
<span>Project:</span> <strong>{report.project_id}</strong>
<span>Project</span>
<strong>{report.project_id}</strong>
</div>
<div className="info-row">
<span>Bug ID:</span> <Link to={`/bugs/${report.error_log_id}`}>#{report.error_log_id}</Link>
<span>Bug ID</span>
<Link to={`/bugs/${report.error_log_id}`}>#{report.error_log_id}</Link>
</div>
<div className="info-row">
<span>Created At:</span> {new Date(report.created_at).toLocaleString()}
<span>Created At</span>
<span>{new Date(report.created_at).toLocaleString()}</span>
</div>
<div className="info-row">
<span>Test Result:</span>
<span style={{ color: report.test_passed ? '#10b981' : '#ef4444', fontWeight: 'bold', marginLeft: '8px' }}>
<span>Test Result</span>
<span className={report.test_passed ? 'test-pass' : 'test-fail'}>
{report.test_passed ? 'PASS' : 'FAIL'}
</span>
</div>
</div>
<div className="card">
<h2>🤖 AI Analysis</h2>
<div className="stack-trace">
<pre style={{ whiteSpace: 'pre-wrap' }}>{report.ai_analysis}</pre>
</div>
<h2><Bot size={16} /> AI Analysis</h2>
<pre className="code-block neutral">{report.ai_analysis}</pre>
</div>
<div className="card">
<h2>📝 Code Changes</h2>
<div className="stack-trace">
<pre>{report.code_diff || 'No changes recorded'}</pre>
</div>
<h2><FileCode size={16} /> Code Changes</h2>
<pre className="code-block neutral">{report.code_diff || 'No changes recorded'}</pre>
</div>
<div className="card">
<h2>🧪 Test Output</h2>
<div className="stack-trace">
<pre>{report.test_output}</pre>
</div>
</div>
<h2><FlaskConical size={16} /> Test Output</h2>
<pre className="code-block neutral">{report.test_output}</pre>
</div>
</div>
);
}
export default RepairDetail;

View File

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { getRepairReports, type RepairReport, getProjects } from '../api';
function RepairList() {
export default function RepairList() {
const [reports, setReports] = useState<RepairReport[]>([]);
const [loading, setLoading] = useState(true);
const [projects, setProjects] = useState<string[]>([]);
@ -13,13 +13,6 @@ function RepairList() {
const [totalPages, setTotalPages] = useState(1);
useEffect(() => {
fetchProjects();
}, []);
useEffect(() => {
fetchReports();
}, [filters]);
const fetchProjects = async () => {
try {
const res = await getProjects();
@ -28,7 +21,10 @@ function RepairList() {
console.error(err);
}
};
fetchProjects();
}, []);
useEffect(() => {
const fetchReports = async () => {
setLoading(true);
try {
@ -44,39 +40,44 @@ function RepairList() {
setLoading(false);
}
};
fetchReports();
}, [filters]);
const handleFilterChange = (key: string, value: any) => {
const handleFilterChange = (key: string, value: string) => {
setFilters((prev) => ({ ...prev, [key]: value, page: 1 }));
};
const getStatusColor = (status: string) => {
switch (status) {
case 'FIXED': return '#10b981';
case 'FIX_FAILED': return '#ef4444';
default: return '#6b7280';
}
};
return (
<div className="bug-list-page">
<div className="header">
<h1>🔧 Repair Reports</h1>
<div>
<div className="page-header">
<div className="title-row">
<div>
<h1 className="page-title">Repair Reports</h1>
<p className="page-subtitle">AI-powered bug repair attempts and their results</p>
</div>
<div className="filters">
<select
className="filter-select"
value={filters.project_id}
onChange={(e) => handleFilterChange('project_id', e.target.value)}
>
<option value="">All Projects</option>
{projects.map((p) => (
<option key={p} value={p}>
{p}
</option>
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
</div>
</div>
<div className="table-container">
{loading ? (
<div className="loading">
<div className="spinner"></div>
</div>
) : reports.length === 0 ? (
<div className="empty-state">No reports found</div>
) : (
<table>
<thead>
<tr>
@ -84,56 +85,49 @@ function RepairList() {
<th>Project</th>
<th>Bug ID</th>
<th>Modified Files</th>
<th>Test Passed</th>
<th>Test Result</th>
<th>Status</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={8} className="text-center">Loading...</td>
</tr>
) : reports.length === 0 ? (
<tr>
<td colSpan={8} className="text-center">No reports found</td>
</tr>
) : (
reports.map((report) => (
{reports.map((report) => (
<tr key={report.id}>
<td>#{report.id}</td>
<td>{report.project_id}</td>
<td>
<Link to={`/bugs/${report.error_log_id}`}>#{report.error_log_id}</Link>
<Link to={`/bugs/${report.error_log_id}`}>
#{report.error_log_id}
</Link>
</td>
<td>{report.modified_files.length} files</td>
<td>
<span style={{ color: report.test_passed ? '#10b981' : '#ef4444' }}>
<span className={report.test_passed ? 'test-pass' : 'test-fail'}>
{report.test_passed ? 'PASS' : 'FAIL'}
</span>
</td>
<td>
<span
className="status-badge"
style={{ backgroundColor: getStatusColor(report.status) }}
>
<span className={`status-badge status-${report.status}`}>
{report.status}
</span>
</td>
<td>{new Date(report.created_at).toLocaleString()}</td>
<td className="cell-secondary">
{new Date(report.created_at).toLocaleString()}
</td>
<td>
<Link to={`/repairs/${report.id}`} className="btn-link">
View
</Link>
</td>
</tr>
))
)}
))}
</tbody>
</table>
)}
</div>
{totalPages > 1 && (
<div className="pagination">
<button
disabled={filters.page === 1}
@ -141,7 +135,7 @@ function RepairList() {
>
Previous
</button>
<span>
<span className="pagination-info">
Page {filters.page} of {totalPages}
</span>
<button
@ -151,8 +145,7 @@ function RepairList() {
Next
</button>
</div>
)}
</div>
);
}
export default RepairList;