import { useState, useEffect } from 'react';
import { ShieldCheck, AlertTriangle, Trash2, RefreshCw, Activity } from 'lucide-react';
interface HealthStats {
status: 'healthy' | 'warning' | 'error';
stats: {
orphaned_nodes: number;
duplicate_nodes: number;
};
issues: Array<{ type: string; message: string }>;
}
export default function MaintenanceTab() {
const [health, setHealth] = useState<HealthStats | null>(null);
const [scanning, setScanning] = useState(false);
const [cleaning, setCleaning] = useState(false);
const [resetting, setResetting] = useState(false);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const scanHealth = async () => {
setScanning(true);
setError(null);
try {
const res = await fetch('/api/maintenance/health');
if (!res.ok) throw new Error('健康检查接口错误');
const data = await res.json();
setHealth(data);
} catch (err) {
console.error(err);
setError('无法获取系统健康信息,请检查 Dashboard Backend / Neo4j 状态。');
} finally {
setScanning(false);
}
};
const cleanOrphans = async () => {
setCleaning(true);
setError(null);
try {
const res = await fetch('/api/maintenance/cleanup/orphans', { method: 'POST' });
if (!res.ok) throw new Error('清理孤立节点失败');
await scanHealth(); // Re-scan after clean
} catch (err) {
console.error(err);
setError('清理孤立节点失败,请稍后重试。');
} finally {
setCleaning(false);
}
};
const resetDatabase = async () => {
setResetting(true);
setError(null);
try {
const res = await fetch('/api/maintenance/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confirm: true })
});
if (!res.ok) throw new Error('重置接口返回错误');
await scanHealth();
setSuccessMessage('数据库已成功重置');
setError(null);
// 3秒后自动清除成功消息
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err) {
console.error(err);
setError('数据库重置失败,请检查服务日志。');
setSuccessMessage(null);
} finally {
setResetting(false);
setShowResetConfirm(false);
}
};
useEffect(() => {
scanHealth();
}, []);
return (
<div className="space-y-8 max-w-5xl">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<ShieldCheck className="w-6 h-6 text-blue-600" />
系统维护与健康
</h2>
<p className="text-muted-foreground text-sm mt-1">
扫描系统健康状况,清理冗余数据,确保图谱运行在最佳状态。
</p>
</div>
<button
onClick={scanHealth}
disabled={scanning}
className="flex items-center gap-2 px-4 py-2 bg-white border rounded-md shadow-sm hover:bg-slate-50 disabled:opacity-50 transition-colors"
>
<RefreshCw className={`w-4 h-4 ${scanning ? 'animate-spin' : ''}`} />
{scanning ? '正在扫描...' : '重新扫描'}
</button>
</div>
{error && (
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">
{error}
</div>
)}
{successMessage && (
<div className="rounded-md border border-green-200 bg-green-50 px-4 py-2 text-sm text-green-700">
{successMessage}
</div>
)}
{/* Health Status Card */}
<div className={`border rounded-xl p-6 shadow-sm transition-all ${
health?.status === 'healthy' ? 'bg-green-50/50 border-green-200' :
health?.status === 'warning' ? 'bg-yellow-50/50 border-yellow-200' : 'bg-red-50/50 border-red-200'
}`}>
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full ${
health?.status === 'healthy' ? 'bg-green-100 text-green-600' :
health?.status === 'warning' ? 'bg-yellow-100 text-yellow-600' : 'bg-red-100 text-red-600'
}`}>
<Activity className="w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold flex items-center gap-2">
当前状态: {health?.status === 'healthy' ? '系统健康' : health?.status === 'warning' ? '需要关注' : '系统异常'}
</h3>
<div className="mt-4 grid grid-cols-2 gap-4">
<div className="bg-white/60 p-3 rounded-lg border border-black/5">
<div className="text-sm text-muted-foreground">孤立节点</div>
<div className="text-2xl font-bold">{health?.stats?.orphaned_nodes ?? '-'}</div>
</div>
<div className="bg-white/60 p-3 rounded-lg border border-black/5">
<div className="text-sm text-muted-foreground">疑似重复节点</div>
<div className="text-2xl font-bold">{health?.stats?.duplicate_nodes ?? '-'}</div>
</div>
</div>
{/* Issues List */}
{health?.issues && health.issues.length > 0 && (
<div className="mt-4 space-y-2">
{health.issues.map((issue, i) => (
<div key={i} className="flex items-center gap-2 text-sm text-yellow-700 bg-yellow-100/50 px-3 py-2 rounded-md border border-yellow-200/50">
<AlertTriangle className="w-4 h-4" />
{issue.message}
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Actions Area */}
<div className="grid md:grid-cols-2 gap-6">
{/* Cleanup Tools */}
<div className="border rounded-xl p-6 bg-card shadow-sm">
<h3 className="font-semibold flex items-center gap-2 mb-4">
<RefreshCw className="w-5 h-5 text-blue-500" />
数据清理
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg border">
<div>
<div className="font-medium text-sm">清理孤立节点</div>
<div className="text-xs text-muted-foreground">删除所有没有关系的实体节点(保留 Episode)</div>
</div>
<button
onClick={cleanOrphans}
disabled={cleaning || (health?.stats?.orphaned_nodes === 0)}
className="px-3 py-1.5 text-xs font-medium bg-white border border-slate-200 rounded hover:bg-slate-50 disabled:opacity-50"
>
{cleaning ? '清理中...' : '立即清理'}
</button>
</div>
</div>
</div>
{/* Danger Zone */}
<div className="border rounded-xl p-6 bg-red-50/30 border-red-100 shadow-sm">
<h3 className="font-semibold flex items-center gap-2 mb-4 text-red-700">
<AlertTriangle className="w-5 h-5" />
危险区域
</h3>
<div className="p-4 bg-white/80 border border-red-100 rounded-lg">
<div className="font-medium text-sm text-red-900">重置数据库</div>
<div className="text-xs text-red-700/70 mt-1 mb-4">
这将删除所有节点和关系,此操作不可恢复。请谨慎操作!
</div>
{!showResetConfirm ? (
<button
onClick={() => setShowResetConfirm(true)}
className="w-full px-4 py-2 bg-red-600 text-white text-sm font-medium rounded hover:bg-red-700 transition-colors"
>
重置所有数据
</button>
) : (
<div className="space-y-2 animate-in fade-in slide-in-from-top-2">
<div className="text-xs text-center font-bold text-red-600">确定要执行吗?</div>
<div className="flex gap-2">
<button
onClick={() => setShowResetConfirm(false)}
className="flex-1 px-3 py-2 bg-slate-100 text-slate-700 text-xs rounded hover:bg-slate-200"
>
取消
</button>
<button
onClick={resetDatabase}
disabled={resetting}
className="flex-1 px-3 py-2 bg-red-600 text-white text-xs rounded hover:bg-red-700 flex items-center justify-center gap-2"
>
<Trash2 className="w-3 h-3" />
{resetting ? '重置中...' : '确认重置'}
</button>
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
}