feat(intelligent-eval): add cron pool monitoring UI (ticket 10)
- Add openclawCronPoolApi with all cron pool API calls - Add CronPoolMonitor page with pool status, metrics, and alerts - Implement real-time refresh (5 second polling) - Implement manual scaling with target size input - Add alert history table with resolve functionality - Add route /cron-pool for cron pool monitoring page - Pass TypeScript type checking All 853 tests passing.
This commit is contained in:
parent
ee639afb0d
commit
244feae505
@ -38,6 +38,7 @@ const OpenClawPage = lazy(() => import('./pages/OpenClaw'))
|
|||||||
const FilesPage = lazy(() => import('./pages/Files'))
|
const FilesPage = lazy(() => import('./pages/Files'))
|
||||||
const ModelConfigsPage = lazy(() => import('./pages/ModelConfigs'))
|
const ModelConfigsPage = lazy(() => import('./pages/ModelConfigs'))
|
||||||
const IntelligentEvalsPage = lazy(() => import('./pages/IntelligentEvals'))
|
const IntelligentEvalsPage = lazy(() => import('./pages/IntelligentEvals'))
|
||||||
|
const CronPoolMonitorPage = lazy(() => import('./pages/CronPoolMonitor'))
|
||||||
|
|
||||||
function PageLoader({ children }: { children: ReactNode }) {
|
function PageLoader({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@ -67,6 +68,7 @@ const routeConfigs: RouteConfig[] = [
|
|||||||
{ path: '/campaigns', name: '评估活动', icon: <ScheduleOutlined />, component: () => <PageLoader><CampaignsPage /></PageLoader> },
|
{ path: '/campaigns', name: '评估活动', icon: <ScheduleOutlined />, component: () => <PageLoader><CampaignsPage /></PageLoader> },
|
||||||
{ path: '/reports', name: '评测报告', icon: <BarChartOutlined />, component: () => <PageLoader><ReportsPage /></PageLoader> },
|
{ path: '/reports', name: '评测报告', icon: <BarChartOutlined />, component: () => <PageLoader><ReportsPage /></PageLoader> },
|
||||||
{ path: '/intelligent-evals', name: '智能评估', icon: <BulbOutlined />, component: () => <PageLoader><IntelligentEvalsPage /></PageLoader> },
|
{ path: '/intelligent-evals', name: '智能评估', icon: <BulbOutlined />, component: () => <PageLoader><IntelligentEvalsPage /></PageLoader> },
|
||||||
|
{ path: '/cron-pool', name: 'Cron 池监控', icon: <DashboardOutlined />, component: () => <PageLoader><CronPoolMonitorPage /></PageLoader> },
|
||||||
{ path: '/models', name: '模型配置', icon: <CloudServerOutlined />, component: () => <PageLoader><ModelConfigsPage /></PageLoader> },
|
{ path: '/models', name: '模型配置', icon: <CloudServerOutlined />, component: () => <PageLoader><ModelConfigsPage /></PageLoader> },
|
||||||
{ path: '/files', name: '原始文件', icon: <FolderOpenOutlined />, component: () => <PageLoader><FilesPage /></PageLoader> },
|
{ path: '/files', name: '原始文件', icon: <FolderOpenOutlined />, component: () => <PageLoader><FilesPage /></PageLoader> },
|
||||||
]
|
]
|
||||||
|
|||||||
@ -792,6 +792,59 @@ export const intelligentEvalsApi = {
|
|||||||
api.get<{ logs: DecisionLog[] }>(`/intelligent-evals/${id}/decision-logs`),
|
api.get<{ logs: DecisionLog[] }>(`/intelligent-evals/${id}/decision-logs`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── OpenClaw Cron Pool ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface CronPoolStatus {
|
||||||
|
total: number
|
||||||
|
idle: number
|
||||||
|
busy: number
|
||||||
|
stuck: number
|
||||||
|
min_size: number
|
||||||
|
max_size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CronPoolMetrics {
|
||||||
|
pool_utilization: number
|
||||||
|
task_backlog: number
|
||||||
|
stuck_rate: number
|
||||||
|
avg_processing_time_seconds: number | null
|
||||||
|
eval_completion_rate: number
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CronPoolAlert {
|
||||||
|
id: string
|
||||||
|
alert_type: string
|
||||||
|
severity: string
|
||||||
|
message: string
|
||||||
|
metric_value: number
|
||||||
|
threshold: number
|
||||||
|
created_at: string
|
||||||
|
resolved_at: string | null
|
||||||
|
webhook_sent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openclawCronPoolApi = {
|
||||||
|
getStatus: () => api.get<{ pool: CronPoolStatus }>('/openclaw/cron-pool'),
|
||||||
|
scale: (targetSize: number) =>
|
||||||
|
api.post<{ success: boolean; scaled_up?: number; scaled_down?: number; current_size: number }>(
|
||||||
|
'/openclaw/cron-pool/scale',
|
||||||
|
{ target_size: targetSize },
|
||||||
|
),
|
||||||
|
sync: () => api.post<{ success: boolean; synced: number }>('/openclaw/cron-pool/sync'),
|
||||||
|
autoScale: () =>
|
||||||
|
api.post<{ success: boolean; scaled_up: number; scaled_down: number }>('/openclaw/cron-pool/auto-scale'),
|
||||||
|
getMetrics: () => api.get<{ metrics: CronPoolMetrics }>('/openclaw/cron-pool/metrics'),
|
||||||
|
checkAlerts: () =>
|
||||||
|
api.post<{ success: boolean; alerts_triggered: number; alerts: CronPoolAlert[] }>('/openclaw/cron-pool/check-alerts'),
|
||||||
|
getAlerts: (limit = 100, unresolvedOnly = false) =>
|
||||||
|
api.get<{ alerts: CronPoolAlert[] }>('/openclaw/cron-pool/alerts', {
|
||||||
|
params: { limit, unresolved_only: unresolvedOnly },
|
||||||
|
}),
|
||||||
|
resolveAlert: (alertId: string) =>
|
||||||
|
api.post<{ success: boolean }>(`/openclaw/cron-pool/alerts/${alertId}/resolve`),
|
||||||
|
}
|
||||||
|
|
||||||
// ── File Management ──────────────────────────────────────────────
|
// ── File Management ──────────────────────────────────────────────
|
||||||
|
|
||||||
export interface FileCategory {
|
export interface FileCategory {
|
||||||
|
|||||||
214
frontend/web/src/pages/CronPoolMonitor.tsx
Normal file
214
frontend/web/src/pages/CronPoolMonitor.tsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Alert, Button, Card, Descriptions, Empty, InputNumber, Space, Statistic, Table, Tag, message,
|
||||||
|
} from 'antd'
|
||||||
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
|
import { ReloadOutlined, WarningOutlined } from '@ant-design/icons'
|
||||||
|
import { openclawCronPoolApi, type CronPoolAlert, type CronPoolMetrics, type CronPoolStatus } from '../api'
|
||||||
|
import { colors } from '../tokens'
|
||||||
|
import { formatDateTime } from '../utils/date'
|
||||||
|
|
||||||
|
export default function CronPoolMonitor() {
|
||||||
|
const [status, setStatus] = useState<CronPoolStatus | null>(null)
|
||||||
|
const [metrics, setMetrics] = useState<CronPoolMetrics | null>(null)
|
||||||
|
const [alerts, setAlerts] = useState<CronPoolAlert[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [scaleTarget, setScaleTarget] = useState<number>(5)
|
||||||
|
const [scaleBusy, setScaleBusy] = useState(false)
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const [statusRes, metricsRes, alertsRes] = await Promise.all([
|
||||||
|
openclawCronPoolApi.getStatus(),
|
||||||
|
openclawCronPoolApi.getMetrics(),
|
||||||
|
openclawCronPoolApi.getAlerts(50),
|
||||||
|
])
|
||||||
|
setStatus(statusRes.data.pool)
|
||||||
|
setMetrics(metricsRes.data.metrics)
|
||||||
|
setAlerts(alertsRes.data.alerts)
|
||||||
|
} catch {
|
||||||
|
message.error('加载数据失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadData()
|
||||||
|
const interval = setInterval(() => void loadData(), 5000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleScale = async () => {
|
||||||
|
setScaleBusy(true)
|
||||||
|
try {
|
||||||
|
await openclawCronPoolApi.scale(scaleTarget)
|
||||||
|
message.success('扩缩容成功')
|
||||||
|
await loadData()
|
||||||
|
} catch {
|
||||||
|
message.error('扩缩容失败')
|
||||||
|
} finally {
|
||||||
|
setScaleBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResolveAlert = async (alertId: string) => {
|
||||||
|
try {
|
||||||
|
await openclawCronPoolApi.resolveAlert(alertId)
|
||||||
|
message.success('已解决告警')
|
||||||
|
await loadData()
|
||||||
|
} catch {
|
||||||
|
message.error('解决告警失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertColumns: ColumnsType<CronPoolAlert> = [
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
render: (val: string) => formatDateTime(val),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '级别',
|
||||||
|
dataIndex: 'severity',
|
||||||
|
key: 'severity',
|
||||||
|
width: 100,
|
||||||
|
render: (val: string) => (
|
||||||
|
<Tag color={val === 'critical' ? 'red' : val === 'warning' ? 'orange' : 'default'}>
|
||||||
|
{val === 'critical' ? '严重' : val === 'warning' ? '警告' : val}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'alert_type',
|
||||||
|
key: 'alert_type',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消息',
|
||||||
|
dataIndex: 'message',
|
||||||
|
key: 'message',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (_, record) => (
|
||||||
|
record.resolved_at ? (
|
||||||
|
<Tag color="green">已解决</Tag>
|
||||||
|
) : (
|
||||||
|
<Button size="small" type="primary" onClick={() => handleResolveAlert(record.id)}>
|
||||||
|
解决
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const unresolvedAlerts = alerts.filter((a) => !a.resolved_at)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||||
|
<span style={{ fontSize: 18, fontWeight: 600 }}>Cron 池监控</span>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{unresolvedAlerts.length > 0 && (
|
||||||
|
<Alert
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
icon={<WarningOutlined />}
|
||||||
|
message={`有 ${unresolvedAlerts.length} 个未解决的告警`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card size="small" title="池状态" style={{ marginBottom: 16 }}>
|
||||||
|
{status ? (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 16 }}>
|
||||||
|
<Statistic title="总数" value={status.total} />
|
||||||
|
<Statistic title="空闲" value={status.idle} valueStyle={{ color: '#52c41a' }} />
|
||||||
|
<Statistic title="忙碌" value={status.busy} valueStyle={{ color: colors.primary }} />
|
||||||
|
<Statistic title="卡死" value={status.stuck} valueStyle={{ color: status.stuck > 0 ? '#ff4d4f' : undefined }} />
|
||||||
|
</div>
|
||||||
|
<Descriptions size="small" column={2}>
|
||||||
|
<Descriptions.Item label="最小池大小">{status.min_size}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="最大池大小">{status.max_size}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Space>
|
||||||
|
<InputNumber
|
||||||
|
min={status.min_size}
|
||||||
|
max={status.max_size}
|
||||||
|
value={scaleTarget}
|
||||||
|
onChange={(val) => val && setScaleTarget(val)}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={handleScale} loading={scaleBusy}>
|
||||||
|
手动扩缩容
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Empty description="加载中..." />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="监控指标" style={{ marginBottom: 16 }}>
|
||||||
|
{metrics ? (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
|
||||||
|
<Statistic
|
||||||
|
title="池使用率"
|
||||||
|
value={(metrics.pool_utilization * 100).toFixed(1)}
|
||||||
|
suffix="%"
|
||||||
|
valueStyle={{
|
||||||
|
color: metrics.pool_utilization > 0.9 ? '#ff4d4f' : metrics.pool_utilization > 0.7 ? colors.warning : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Statistic title="任务积压" value={metrics.task_backlog} />
|
||||||
|
<Statistic
|
||||||
|
title="卡死率"
|
||||||
|
value={(metrics.stuck_rate * 100).toFixed(1)}
|
||||||
|
suffix="%"
|
||||||
|
valueStyle={{ color: metrics.stuck_rate > 0.1 ? '#ff4d4f' : undefined }}
|
||||||
|
/>
|
||||||
|
<Statistic
|
||||||
|
title="平均处理时间"
|
||||||
|
value={metrics.avg_processing_time_seconds ? (metrics.avg_processing_time_seconds / 60).toFixed(1) : '—'}
|
||||||
|
suffix={metrics.avg_processing_time_seconds ? '分钟' : ''}
|
||||||
|
/>
|
||||||
|
<Statistic
|
||||||
|
title="评估完成率"
|
||||||
|
value={(metrics.eval_completion_rate * 100).toFixed(1)}
|
||||||
|
suffix="%"
|
||||||
|
/>
|
||||||
|
<Statistic title="更新时间" value={formatDateTime(metrics.timestamp)} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Empty description="加载中..." />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="告警历史">
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
dataSource={alerts}
|
||||||
|
columns={alertColumns}
|
||||||
|
pagination={false}
|
||||||
|
locale={{ emptyText: <Empty description="暂无告警" /> }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user