+мониторинг отображение

This commit is contained in:
dv 2026-05-06 11:35:24 +03:00
parent 011a10641f
commit b230025ab4
5 changed files with 200 additions and 6 deletions

View file

@ -472,6 +472,20 @@ async def run_dashboard_monitor_cycle(
logger.info("Dashboard monitor: %d devices loaded for checking", len(device_rows))
checks = (await db.execute(select(DashboardCheckDefinition))).scalars().all()
await sse_manager.publish(
SSEEvent(
type="dashboard_monitor_cycle_start",
task_id=DASHBOARD_MONITOR_CHANNEL,
data={
"run_type": run_type,
"objects_allowed": len(allowed_object_ids),
"devices_total": len(device_rows),
"checks_planned": [c.check_key for c in checks if c.enabled],
"ts": datetime.now(timezone.utc).isoformat(),
},
)
)
overrides = (
await db.execute(
select(DashboardObjectCheckOverride).where(
@ -625,6 +639,7 @@ async def run_dashboard_monitor_cycle(
now=checked_at,
)
check_failed = sum(1 for r in results if isinstance(r, Exception) or (not isinstance(r, Exception) and not r[0]))
await sse_manager.publish(
SSEEvent(
type="dashboard_monitor_update",
@ -632,6 +647,8 @@ async def run_dashboard_monitor_cycle(
data={
"check_key": check.check_key,
"processed": len(due_devices),
"due": len(due_devices),
"failed": check_failed,
"ts": datetime.now(timezone.utc).isoformat(),
},
)

View file

@ -41,6 +41,12 @@ export interface DashboardMonitorStatus {
last_counts: Record<string, unknown> | null
}
export interface DashboardRunResponse {
accepted: boolean
running: boolean
detail: string
}
export interface DashboardObjectPolicy {
object_id: number
mode: 'inherit' | 'include' | 'exclude'
@ -249,7 +255,7 @@ export const useDashboardRunNow = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: DashboardScope & { force?: boolean }) =>
api.post('/api/v1/dashboard/monitor/run', body).then((r) => r.data),
api.post<DashboardRunResponse>('/api/v1/dashboard/monitor/run', body).then((r) => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['dashboard-monitor-status'] })
qc.invalidateQueries({ queryKey: ['dashboard-home-summary'] })

View file

@ -24,6 +24,7 @@ export function useDashboardMonitorSSE(handlers: DashboardMonitorHandlers) {
try {
const data = JSON.parse(ev.data)
if (
ev.event === 'dashboard_monitor_cycle_start' ||
ev.event === 'dashboard_monitor_update' ||
ev.event === 'dashboard_monitor_cycle_done' ||
ev.event === 'dashboard_monitor_cycle_failed'

View file

@ -124,8 +124,12 @@ export function HomePage() {
const handleRunNow = async () => {
try {
await runNow.mutateAsync({ ...scope, force: true })
message.success('Мониторинг запущен')
const result = await runNow.mutateAsync({ ...scope, force: true })
if (!result.accepted) {
message.warning('Мониторинг уже запущен')
} else {
message.success('Мониторинг запущен')
}
} catch {
message.error('Не удалось запустить мониторинг')
}

View file

@ -1,12 +1,14 @@
import { ArrowLeftOutlined } from '@ant-design/icons'
import { ArrowLeftOutlined, CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'
import {
Alert,
Badge,
Button,
Card,
Checkbox,
Col,
Collapse,
InputNumber,
Progress,
Row,
Space,
Switch,
@ -27,6 +29,7 @@ import {
usePatchDashboardObjectPolicy,
usePatchDashboardTagPolicy,
} from '../api/dashboard'
import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE'
import { useTags } from '../api/tags'
function cityKey(city: string | null): string {
@ -39,6 +42,36 @@ function formatCheckTitle(checkKey: string): string {
return checkKey
}
interface CheckProgress {
processed: number
due: number
failed: number
}
interface RunProgress {
phase: 'idle' | 'running' | 'done' | 'failed'
startedAt: string | null
objectsAllowed: number | null
devicesTotal: number | null
checksPlanned: string[]
checkProgress: Record<string, CheckProgress>
counts: Record<string, number> | null
error: string | null
finishedAt: string | null
}
const EMPTY_PROGRESS: RunProgress = {
phase: 'idle',
startedAt: null,
objectsAllowed: null,
devicesTotal: null,
checksPlanned: [],
checkProgress: {},
counts: null,
error: null,
finishedAt: null,
}
export function HomeSettingsPage() {
const navigate = useNavigate()
@ -54,6 +87,52 @@ export function HomeSettingsPage() {
const patchChecks = usePatchDashboardChecks()
const [checkIntervalsMin, setCheckIntervalsMin] = useState<Record<string, number>>({})
const [runProgress, setRunProgress] = useState<RunProgress>(EMPTY_PROGRESS)
useDashboardMonitorSSE({
onUpdate: (eventType, data) => {
if (eventType === 'dashboard_monitor_cycle_start') {
setRunProgress({
phase: 'running',
startedAt: (data.ts as string) ?? null,
objectsAllowed: (data.objects_allowed as number) ?? null,
devicesTotal: (data.devices_total as number) ?? null,
checksPlanned: (data.checks_planned as string[]) ?? [],
checkProgress: {},
counts: null,
error: null,
finishedAt: null,
})
} else if (eventType === 'dashboard_monitor_update') {
const key = data.check_key as string
setRunProgress((prev) => ({
...prev,
checkProgress: {
...prev.checkProgress,
[key]: {
processed: (data.processed as number) ?? 0,
due: (data.due as number) ?? 0,
failed: (data.failed as number) ?? 0,
},
},
}))
} else if (eventType === 'dashboard_monitor_cycle_done') {
setRunProgress((prev) => ({
...prev,
phase: 'done',
counts: (data.counts as Record<string, number>) ?? null,
finishedAt: (data.ts as string) ?? null,
}))
} else if (eventType === 'dashboard_monitor_cycle_failed') {
setRunProgress((prev) => ({
...prev,
phase: 'failed',
error: (data.error as string) ?? 'Неизвестная ошибка',
finishedAt: (data.ts as string) ?? null,
}))
}
},
})
useEffect(() => {
const next: Record<string, number> = {}
@ -174,8 +253,12 @@ export function HomeSettingsPage() {
const handleRunNow = async () => {
try {
await runNow.mutateAsync({ force: true })
message.success('Мониторинг запущен')
const result = await runNow.mutateAsync({ force: true })
if (!result.accepted) {
message.warning('Мониторинг уже запущен')
} else {
message.success('Мониторинг запущен')
}
} catch {
message.error('Не удалось запустить мониторинг')
}
@ -216,6 +299,89 @@ export function HomeSettingsPage() {
<Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} />
) : null}
{runProgress.phase !== 'idle' && (
<Card
style={{ marginBottom: 16 }}
title={
<Space>
{runProgress.phase === 'running' && <LoadingOutlined style={{ color: '#1677ff' }} />}
{runProgress.phase === 'done' && <CheckCircleOutlined style={{ color: '#52c41a' }} />}
{runProgress.phase === 'failed' && <CloseCircleOutlined style={{ color: '#ff4d4f' }} />}
<Typography.Text strong>Текущий прогон</Typography.Text>
<Badge
status={runProgress.phase === 'running' ? 'processing' : runProgress.phase === 'done' ? 'success' : 'error'}
text={runProgress.phase === 'running' ? 'выполняется' : runProgress.phase === 'done' ? 'завершён' : 'ошибка'}
/>
</Space>
}
>
<Space direction="vertical" style={{ width: '100%' }} size={8}>
<Typography.Text type="secondary">
{runProgress.objectsAllowed != null && `Объектов: ${runProgress.objectsAllowed}`}
{runProgress.devicesTotal != null && ` • Устройств: ${runProgress.devicesTotal}`}
{runProgress.startedAt && ` • Начат: ${dayjs(runProgress.startedAt).format('HH:mm:ss')}`}
{runProgress.finishedAt && ` • Завершён: ${dayjs(runProgress.finishedAt).format('HH:mm:ss')}`}
</Typography.Text>
{runProgress.checksPlanned.map((checkKey) => {
const cp = runProgress.checkProgress[checkKey]
const due = cp?.due ?? 0
const processed = cp?.processed ?? 0
const failed = cp?.failed ?? 0
const pct = due > 0 ? Math.round((processed / due) * 100) : runProgress.phase === 'done' ? 100 : 0
return (
<div key={checkKey}>
<Row justify="space-between" style={{ marginBottom: 2 }}>
<Col>
<Typography.Text>{formatCheckTitle(checkKey)}</Typography.Text>
{failed > 0 && (
<Tag color="red" style={{ marginLeft: 8 }}>
{failed} ошибок
</Tag>
)}
</Col>
<Col>
<Typography.Text type="secondary">
{cp ? `${processed} / ${due}` : '—'}
</Typography.Text>
</Col>
</Row>
<Progress
percent={pct}
status={runProgress.phase === 'failed' ? 'exception' : failed > 0 ? 'normal' : undefined}
strokeColor={failed > 0 ? '#faad14' : undefined}
size="small"
/>
</div>
)
})}
{runProgress.phase === 'done' && runProgress.counts && (
<Row gutter={16} style={{ marginTop: 8 }}>
<Col>
<Typography.Text type="secondary">Проверено: </Typography.Text>
<Typography.Text strong>{runProgress.counts.devices_checked ?? 0}</Typography.Text>
</Col>
<Col>
<Typography.Text type="secondary">Ошибок: </Typography.Text>
<Typography.Text strong style={{ color: (runProgress.counts.failed ?? 0) > 0 ? '#ff4d4f' : undefined }}>
{runProgress.counts.failed ?? 0}
</Typography.Text>
</Col>
<Col>
<Typography.Text type="secondary">Пропущено: </Typography.Text>
<Typography.Text strong>{runProgress.counts.skipped_not_due ?? 0}</Typography.Text>
</Col>
</Row>
)}
{runProgress.phase === 'failed' && runProgress.error && (
<Alert type="error" showIcon message={runProgress.error} style={{ marginTop: 8 }} />
)}
</Space>
</Card>
)}
<Card title="Плагины мониторинга" style={{ marginBottom: 16 }} loading={checksLoading}>
{(checksData?.checks ?? []).length === 0 ? (
<Alert