From 244feae5059e4106b8909e0bba1436733154aa29 Mon Sep 17 00:00:00 2001 From: sinohqb Date: Wed, 12 Aug 2026 11:05:51 +0800 Subject: [PATCH] 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. --- frontend/web/src/App.tsx | 2 + frontend/web/src/api.ts | 53 +++++ frontend/web/src/pages/CronPoolMonitor.tsx | 214 +++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 frontend/web/src/pages/CronPoolMonitor.tsx diff --git a/frontend/web/src/App.tsx b/frontend/web/src/App.tsx index b868969..a2be86f 100644 --- a/frontend/web/src/App.tsx +++ b/frontend/web/src/App.tsx @@ -38,6 +38,7 @@ const OpenClawPage = lazy(() => import('./pages/OpenClaw')) const FilesPage = lazy(() => import('./pages/Files')) const ModelConfigsPage = lazy(() => import('./pages/ModelConfigs')) const IntelligentEvalsPage = lazy(() => import('./pages/IntelligentEvals')) +const CronPoolMonitorPage = lazy(() => import('./pages/CronPoolMonitor')) function PageLoader({ children }: { children: ReactNode }) { return ( @@ -67,6 +68,7 @@ const routeConfigs: RouteConfig[] = [ { path: '/campaigns', name: '评估活动', icon: , component: () => }, { path: '/reports', name: '评测报告', icon: , component: () => }, { path: '/intelligent-evals', name: '智能评估', icon: , component: () => }, + { path: '/cron-pool', name: 'Cron 池监控', icon: , component: () => }, { path: '/models', name: '模型配置', icon: , component: () => }, { path: '/files', name: '原始文件', icon: , component: () => }, ] diff --git a/frontend/web/src/api.ts b/frontend/web/src/api.ts index 70a6e3f..0ae6d0e 100644 --- a/frontend/web/src/api.ts +++ b/frontend/web/src/api.ts @@ -792,6 +792,59 @@ export const intelligentEvalsApi = { 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 ────────────────────────────────────────────── export interface FileCategory { diff --git a/frontend/web/src/pages/CronPoolMonitor.tsx b/frontend/web/src/pages/CronPoolMonitor.tsx new file mode 100644 index 0000000..a963e46 --- /dev/null +++ b/frontend/web/src/pages/CronPoolMonitor.tsx @@ -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(null) + const [metrics, setMetrics] = useState(null) + const [alerts, setAlerts] = useState([]) + const [loading, setLoading] = useState(false) + const [scaleTarget, setScaleTarget] = useState(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 = [ + { + title: '时间', + dataIndex: 'created_at', + key: 'created_at', + width: 180, + render: (val: string) => formatDateTime(val), + }, + { + title: '级别', + dataIndex: 'severity', + key: 'severity', + width: 100, + render: (val: string) => ( + + {val === 'critical' ? '严重' : val === 'warning' ? '警告' : val} + + ), + }, + { + 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 ? ( + 已解决 + ) : ( + + ) + ), + }, + ] + + const unresolvedAlerts = alerts.filter((a) => !a.resolved_at) + + return ( +
+
+ Cron 池监控 +
+ +
+ + {unresolvedAlerts.length > 0 && ( + } + message={`有 ${unresolvedAlerts.length} 个未解决的告警`} + /> + )} + + + {status ? ( +
+
+ + + + 0 ? '#ff4d4f' : undefined }} /> +
+ + {status.min_size} + {status.max_size} + +
+ + val && setScaleTarget(val)} + /> + + +
+
+ ) : ( + + )} +
+ + + {metrics ? ( +
+ 0.9 ? '#ff4d4f' : metrics.pool_utilization > 0.7 ? colors.warning : undefined, + }} + /> + + 0.1 ? '#ff4d4f' : undefined }} + /> + + + +
+ ) : ( + + )} +
+ + + }} + /> + + + ) +}