-
- {onBack &&
} onClick={onBack}>返回详情}
-
- {ev.name} · 评估报告
-
-
- {report && (
+
} loading={exporting} onClick={exportMarkdown}>
导出 Markdown
)}
-
+ />
{reportLoading &&
}
diff --git a/frontend/web/src/pages/CronPoolMonitor.test.tsx b/frontend/web/src/pages/CronPoolMonitor.test.tsx
deleted file mode 100644
index f0abb26..0000000
--- a/frontend/web/src/pages/CronPoolMonitor.test.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-// @vitest-environment jsdom
-import { act, cleanup, render } from '@testing-library/react'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-
-vi.mock('../api', () => ({
- openclawCronPoolApi: {
- getStatus: vi.fn(),
- getMetrics: vi.fn(),
- getAlerts: vi.fn(),
- scale: vi.fn(),
- resolveAlert: vi.fn(),
- },
-}))
-
-vi.mock('antd', async () => {
- const actual = await vi.importActual
('antd')
- return {
- ...actual,
- message: { error: vi.fn(), success: vi.fn(), warning: vi.fn(), info: vi.fn() },
- }
-})
-
-import { openclawCronPoolApi } from '../api'
-import CronPoolMonitor from './CronPoolMonitor'
-
-const statusResp = {
- data: { pool: { total: 5, idle: 3, busy: 2, stuck: 0, min_size: 5, max_size: 20 } },
-}
-const metricsResp = { data: { metrics: { pool_utilization: 0.4, task_backlog: 0, stuck_rate: 0 } } }
-const alertsResp = { data: { alerts: [] } }
-
-beforeEach(() => {
- Object.defineProperty(window, 'matchMedia', {
- writable: true,
- value: vi.fn().mockImplementation((query: string) => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: vi.fn(),
- removeListener: vi.fn(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- dispatchEvent: vi.fn(),
- })),
- })
- vi.useFakeTimers()
- vi.mocked(openclawCronPoolApi.getStatus).mockResolvedValue(statusResp as never)
- vi.mocked(openclawCronPoolApi.getMetrics).mockResolvedValue(metricsResp as never)
- vi.mocked(openclawCronPoolApi.getAlerts).mockResolvedValue(alertsResp as never)
-})
-
-afterEach(() => {
- vi.useRealTimers()
- cleanup()
-})
-
-async function settle() {
- await act(async () => {
- await Promise.resolve()
- })
-}
-
-describe('CronPoolMonitor polling lifecycle', () => {
- it('polls every 5 seconds while mounted', async () => {
- render()
- await settle()
- expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(1)
- await act(async () => {
- await vi.advanceTimersByTimeAsync(5000)
- })
- await settle()
- expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(2)
- await act(async () => {
- await vi.advanceTimersByTimeAsync(5000)
- })
- await settle()
- expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(3)
- })
-
- it('clears the polling interval on unmount', async () => {
- const { unmount } = render()
- await settle()
- await act(async () => {
- await vi.advanceTimersByTimeAsync(5000)
- })
- await settle()
- const before = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
- unmount()
- await act(async () => {
- await Promise.resolve()
- await Promise.resolve()
- })
- await act(async () => {
- await vi.advanceTimersByTimeAsync(30000)
- })
- await settle()
- const after = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
- expect(after).toBe(before)
- })
- it('pauses polling while the document is hidden and resumes on visible', async () => {
- // Default starting state is visible.
- vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible')
-
- render()
- await settle()
- const initial = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
- expect(initial).toBeGreaterThanOrEqual(1)
-
- // While hidden, advancing the clock must NOT trigger another poll.
- vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('hidden')
- document.dispatchEvent(new Event('visibilitychange'))
- await act(async () => {
- await vi.advanceTimersByTimeAsync(15000)
- })
- await settle()
- const afterHidden = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
- expect(afterHidden).toBe(initial)
-
- // Returning to visible resumes polling and triggers an immediate reload.
- vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible')
- document.dispatchEvent(new Event('visibilitychange'))
- await settle()
- const afterVisible = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
- expect(afterVisible).toBeGreaterThan(initial)
- })
-})
diff --git a/frontend/web/src/pages/CronPoolMonitor.tsx b/frontend/web/src/pages/CronPoolMonitor.tsx
deleted file mode 100644
index 9f6d861..0000000
--- a/frontend/web/src/pages/CronPoolMonitor.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-// DEPRECATED (ADR-0009): 智能评估已改为触发式执行,cron 池不再使用(worker cron 已禁用)。
-// 本页已从导航移除,遗留保留仅供回溯。监控职责由 TaskQueueMonitor(任务队列页)承担。
-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 { usePolling } from '../hooks/usePolling'
-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)
- }
- }
-
- // Initial fetch (usePolling owns the 5s interval + visibility pause).
- useEffect(() => {
- void loadData()
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- // S6: defer the recurring 5s poll to the shared `usePolling` hook. The
- // default pauses the timer while the document is hidden (background tab /
- // minimised window) and re-fetches once on the way back to `visible`.
- usePolling(() => { void loadData() }, 5000, true)
-
- 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 池监控
-
-
} onClick={() => void loadData()} loading={loading}>
- 刷新
-
-
-
- {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 }}
- />
-
-
-
-
- ) : (
-
- )}
-
-
-
- }}
- />
-
-
- )
-}