import { useEffect, useState } from 'react' import { Card, Row, Col, Statistic, Typography, Button, Tag, Spin, Space, Modal, Alert, Select } from 'antd' import { DatabaseOutlined, BarChartOutlined, AlertOutlined, GlobalOutlined, PoweroffOutlined, WifiOutlined, DisconnectOutlined, ReloadOutlined, } from '@ant-design/icons' import { Link } from 'react-router-dom' import axios from 'axios' import { useAuthStore } from '../../stores/auth' import AppLayout from '../../components/AppLayout/AppLayout' import Scrollbar from '../../components/Scrollbar/Scrollbar' import { useWebSocket } from '../../hooks/useWebSocket' import { formatDateTimeZhCN } from '../../utils/datetime' const { Title, Text } = Typography interface Stats { total_datasources: number active_datasources: number tasks_today: number success_rate: number last_updated: string alerts: { critical: number warning: number info: number } } interface RestartTask { task_id: string action: string status: string stage: string message: string created_at: string updated_at: string } interface RestartTaskLogs { task_id: string lines: string[] } type RestartAction = 'restart-backend' | 'restart-ai-provider' | 'restart-database' | 'restart-system' type RestartStage = 'confirming' | 'waiting_for_shutdown' | 'waiting_for_recovery' | 'recovered' | 'failed' | 'timeout' const RESTART_ACTION_OPTIONS: Array<{ value: RestartAction; label: string; description: string; command: string }> = [ { value: 'restart-backend', label: '重启后端', description: '只重启后端服务,页面通常会短暂失联后自动恢复。', command: './planet.sh restart -b', }, { value: 'restart-ai-provider', label: '重启 AI Provider', description: '只重启 AI Provider 适配服务,前端页面通常保持在线。', command: './planet.sh restart -a', }, { value: 'restart-database', label: '重启数据库', description: '重启 PostgreSQL 和 Redis 容器,前端页面保持在线。', command: './planet.sh restart -d', }, { value: 'restart-system', label: '完全重启', description: '重启前后端和相关服务,页面会短暂不可用,恢复后自动刷新。', command: './planet.sh restart', }, ] const RESTART_GUIDE_LINES: Record = { 'restart-backend': [ '[ctl] preparing backend restart task', '[ctl] handing restart to detached runner', '[ctl] waiting for backend health recovery', ], 'restart-ai-provider': [ '[ctl] preparing ai provider restart task', '[ctl] handing restart to detached runner', '[ctl] waiting for ai provider health recovery', ], 'restart-database': [ '[ctl] preparing database restart task', '[ctl] restarting PostgreSQL and Redis containers', '[ctl] waiting for containers to settle', ], 'restart-system': [ '[ctl] preparing full system restart', '[ctl] notifying operator that frontend may disconnect', '[ctl] stopping frontend and backend services', '[ctl] restarting platform services', '[ctl] polling for frontend re-entry window', ], } let cachedDashboardStats: Stats | null = null function getRestartConfirmMessage(action: RestartAction): string { if (action === 'restart-ai-provider') { return '将重启 AI Provider 适配服务,页面通常保持在线,但 AI 分析请求会短暂不可用。' } if (action === 'restart-database') { return '将重启 PostgreSQL 和 Redis,页面通常保持在线,但相关请求可能短暂波动。' } if (action === 'restart-system') { return '将完全重启前后端和相关服务,页面会短暂不可用,恢复后会自动刷新。' } return '将重启后端服务,页面会短暂不可用。' } function Dashboard() { const { token, clearAuth, user } = useAuthStore() const [stats, setStats] = useState(cachedDashboardStats) const [loading, setLoading] = useState(cachedDashboardStats === null) const [wsConnected, setWsConnected] = useState(false) const [error, setError] = useState(null) const [restartModalOpen, setRestartModalOpen] = useState(false) const [restartSubmitting, setRestartSubmitting] = useState(false) const [restartAction, setRestartAction] = useState('restart-backend') const [restartTaskId, setRestartTaskId] = useState(null) const [restartMessage, setRestartMessage] = useState(getRestartConfirmMessage('restart-backend')) const [restartStage, setRestartStage] = useState('confirming') const [restartLogs, setRestartLogs] = useState([]) const [restartStartedAt, setRestartStartedAt] = useState(null) const isSuperAdmin = user?.role === 'super_admin' const selectedRestartAction = RESTART_ACTION_OPTIONS.find((item) => item.value === restartAction) ?? RESTART_ACTION_OPTIONS[0] useEffect(() => { if (!token) return const fetchStats = async () => { try { if (!cachedDashboardStats) { setLoading(true) } const res = await fetch('/api/v1/dashboard/stats', { headers: { Authorization: `Bearer ${token}` }, }) if (res.status === 401) { clearAuth() window.location.href = '/' return } const data = await res.json() cachedDashboardStats = data setStats(data) setError(null) } catch (err) { setError('获取数据失败') console.error(err) } finally { setLoading(false) } } fetchStats() }, [token, clearAuth]) const { connected: dashboardSocketConnected } = useWebSocket({ autoConnect: true, autoSubscribe: ['dashboard'], onMessage: (message) => { if (message.type === 'data_frame' && message.channel === 'dashboard' && message.payload?.stats) { const nextStats = message.payload.stats as Stats cachedDashboardStats = nextStats setStats(nextStats) } }, }) useEffect(() => { setWsConnected(dashboardSocketConnected) }, [dashboardSocketConnected]) const handleRetry = () => { window.location.reload() } const openRestartModal = () => { setRestartAction('restart-backend') setRestartTaskId(null) setRestartLogs([]) setRestartSubmitting(false) setRestartStartedAt(null) setRestartStage('confirming') setRestartMessage(getRestartConfirmMessage('restart-backend')) setRestartModalOpen(true) } const closeRestartModal = () => { if (restartSubmitting || restartStage === 'waiting_for_shutdown' || restartStage === 'waiting_for_recovery') { return } setRestartModalOpen(false) } const handleRestartAction = async () => { setRestartSubmitting(true) setRestartLogs(RESTART_GUIDE_LINES[restartAction].slice(0, restartAction === 'restart-system' ? 3 : 1)) try { const res = await axios.post('/api/v1/system/restart-tasks', { action: restartAction }) setRestartTaskId(res.data.task_id) setRestartStartedAt(Date.now()) setRestartStage('waiting_for_shutdown') setRestartMessage( restartAction === 'restart-system' ? '已发送完全重启指令,页面可能暂时失联,恢复后会自动刷新。' : restartAction === 'restart-ai-provider' ? '已发送 AI Provider 重启指令,正在等待 AI 服务恢复。' : '已发送重启指令,正在等待服务进入重启流程。' ) setRestartLogs((current) => [...current, `任务已创建: ${res.data.task_id}`]) } catch (restartError: unknown) { const err = restartError as { response?: { data?: { detail?: string } } } setRestartStage('failed') setRestartMessage(err.response?.data?.detail || '提交重启任务失败') setRestartLogs((current) => [...current, '提交重启任务失败']) } finally { setRestartSubmitting(false) } } useEffect(() => { if (!restartModalOpen || restartStage !== 'confirming') return setRestartMessage(getRestartConfirmMessage(restartAction)) }, [restartAction, restartModalOpen, restartStage]) useEffect(() => { if (!restartModalOpen || !restartTaskId || restartStartedAt === null) return let cancelled = false let sawUnhealthy = false let healthyStreak = 0 let frontendHealthyStreak = 0 let pollTimer: number | null = null const appendLog = (line: string) => { setRestartLogs((current) => (current[current.length - 1] === line ? current : [...current, line].slice(-8))) } const poll = async () => { if (cancelled) return const elapsed = Date.now() - restartStartedAt if (elapsed > 90_000) { setRestartStage('timeout') setRestartMessage('恢复超时,请手动检查后端服务状态。') appendLog('恢复超时,请手动检查服务状态') return } try { const taskRes = await axios.get(`/api/v1/system/restart-tasks/${restartTaskId}`, { timeout: 1500 }) const task = taskRes.data if (!cancelled && task?.message) { setRestartMessage(task.message) } if (!cancelled && restartAction !== 'restart-system') { const logsRes = await axios.get(`/api/v1/system/restart-tasks/${restartTaskId}/logs`, { timeout: 1500 }) if (logsRes.data.lines.length > 0) { setRestartLogs(logsRes.data.lines.slice(-8)) } } if (!cancelled && typeof task?.stage === 'string') { if (task.stage === 'healthy') { setRestartStage('recovered') setRestartMessage('服务已恢复,正在刷新页面。') appendLog('后端已恢复,正在刷新页面') window.setTimeout(() => window.location.reload(), 600) return } if (task.status === 'failed' || task.status === 'timeout') { setRestartStage(task.status === 'timeout' ? 'timeout' : 'failed') setRestartMessage(task.message || '重启任务失败') appendLog(task.message || '重启任务失败') return } } } catch { // Backend may be temporarily down during restart; handled by health polling below. } if (restartAction === 'restart-system') { try { const rootRes = await fetch(`/?restart_probe=${Date.now()}`, { cache: 'no-store' }) if (rootRes.ok) { frontendHealthyStreak += 1 if (sawUnhealthy && frontendHealthyStreak >= 2) { setRestartStage('recovered') setRestartMessage('系统已恢复,正在刷新页面。') appendLog('[ctl] frontend entrypoint reachable again') window.setTimeout(() => window.location.reload(), 600) return } } else { sawUnhealthy = true frontendHealthyStreak = 0 setRestartStage('waiting_for_recovery') setRestartMessage('系统正在完全重启,正在等待前端恢复访问。') appendLog('[ctl] frontend is temporarily unavailable') } } catch { sawUnhealthy = true frontendHealthyStreak = 0 setRestartStage('waiting_for_recovery') setRestartMessage('系统正在完全重启,正在等待前端恢复访问。') appendLog('[ctl] frontend is temporarily unavailable') } pollTimer = window.setTimeout(poll, 1500) return } try { const healthRes = await fetch('/health', { cache: 'no-store' }) if (healthRes.ok) { healthyStreak += 1 if (sawUnhealthy && healthyStreak >= 2) { setRestartStage('recovered') setRestartMessage('服务已恢复,正在刷新页面。') appendLog('健康检查已恢复,正在刷新页面') window.setTimeout(() => window.location.reload(), 600) return } } else { sawUnhealthy = true healthyStreak = 0 setRestartStage('waiting_for_recovery') setRestartMessage('后端已停止响应,正在等待服务恢复。') appendLog('检测到后端已停止响应') } } catch { sawUnhealthy = true healthyStreak = 0 setRestartStage('waiting_for_recovery') setRestartMessage('后端已停止响应,正在等待服务恢复。') appendLog('检测到后端已停止响应') } pollTimer = window.setTimeout(poll, 1500) } pollTimer = window.setTimeout(poll, 1200) return () => { cancelled = true if (pollTimer !== null) { window.clearTimeout(pollTimer) } } }, [restartAction, restartModalOpen, restartStartedAt, restartTaskId]) if (loading && !stats) { return (
) } return (
仪表盘 系统总览与实时态势
{wsConnected ? ( } color="success">实时连接 ) : ( } color="default">离线 )} {isSuperAdmin ? ( ) : null}
{error && ( {error} )} } /> } />
快捷入口 快速访问地球可视化页面
} /> } /> } />
{stats?.last_updated && (
最后更新: {formatDateTimeZhCN(stats.last_updated)} {wsConnected && 实时同步中}
)} {restartStage === 'confirming' ? '取消' : '关闭'} , , ]} >
重启动作