кодировка

This commit is contained in:
dv 2026-05-05 15:00:06 +03:00
parent ecb929d780
commit b282de5355

View file

@ -1,4 +1,4 @@
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { import {
Alert, Alert,
Button, Button,
@ -24,10 +24,10 @@ import { useTags } from '../api/tags'
import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE' import { useDashboardMonitorSSE } from '../hooks/useDashboardMonitorSSE'
function formatAge(seconds: number | null | undefined): string { function formatAge(seconds: number | null | undefined): string {
if (seconds == null) return '—' if (seconds == null) return '—'
if (seconds < 60) return `${seconds}ñ` if (seconds < 60) return `${seconds}с`
if (seconds < 3600) return `${Math.floor(seconds / 60)}ì` if (seconds < 3600) return `${Math.floor(seconds / 60)}м`
return `${Math.floor(seconds / 3600)}÷ ${Math.floor((seconds % 3600) / 60)}ì` return `${Math.floor(seconds / 3600)}ч ${Math.floor((seconds % 3600) / 60)}м`
} }
function scopeToSearch(scope: DashboardScope): string { function scopeToSearch(scope: DashboardScope): string {
@ -91,9 +91,9 @@ export function HomePage() {
const handleRunNow = async () => { const handleRunNow = async () => {
try { try {
await runNow.mutateAsync({ ...scope, force: true }) await runNow.mutateAsync({ ...scope, force: true })
message.success('Ìîíèòîðèíã çàïóùåí') message.success('Мониторинг запущен')
} catch { } catch {
message.error('Íå óäàëîñü çàïóñòèòü ìîíèòîðèíã') message.error('Не удалось запустить мониторинг')
} }
} }
@ -203,13 +203,13 @@ export function HomePage() {
<Typography.Title level={4} style={{ margin: 0 }}> <Typography.Title level={4} style={{ margin: 0 }}>
NOC Dashboard NOC Dashboard
</Typography.Title> </Typography.Title>
<Typography.Text type="secondary">SLA, ïîêðûòèå ìîíèòîðèíãîì, èíöèäåíòû è î÷åðåäü äåéñòâèé</Typography.Text> <Typography.Text type="secondary">SLA, покрытие мониторингом, инциденты и очередь действий</Typography.Text>
</Col> </Col>
<Col> <Col>
<Space wrap> <Space wrap>
<Button onClick={() => navigate(`/home/settings${scopeToSearch(scope)}`)}>Íàñòðîéêè ìîíèòîðèíãà</Button> <Button onClick={() => navigate(`/home/settings${scopeToSearch(scope)}`)}>Настройки мониторинга</Button>
<Button type="primary" onClick={handleRunNow} loading={runNow.isPending}> <Button type="primary" onClick={handleRunNow} loading={runNow.isPending}>
Çàïóñòèòü ñåé÷àñ Запустить сейчас
</Button> </Button>
</Space> </Space>
</Col> </Col>
@ -219,7 +219,7 @@ export function HomePage() {
<Row gutter={[8, 8]}> <Row gutter={[8, 8]}>
<Col xs={24} sm={12} lg={6}> <Col xs={24} sm={12} lg={6}>
<Select <Select
placeholder="Ãîðîä" placeholder="Город"
allowClear allowClear
style={{ width: '100%' }} style={{ width: '100%' }}
options={(meta?.cities ?? []).map((c) => ({ value: c, label: c }))} options={(meta?.cities ?? []).map((c) => ({ value: c, label: c }))}
@ -229,7 +229,7 @@ export function HomePage() {
</Col> </Col>
<Col xs={24} sm={12} lg={6}> <Col xs={24} sm={12} lg={6}>
<Select <Select
placeholder="Êëèåíò" placeholder="Клиент"
allowClear allowClear
style={{ width: '100%' }} style={{ width: '100%' }}
options={(meta?.clients ?? []).map((c) => ({ value: c, label: c }))} options={(meta?.clients ?? []).map((c) => ({ value: c, label: c }))}
@ -240,7 +240,7 @@ export function HomePage() {
<Col xs={24} sm={12} lg={6}> <Col xs={24} sm={12} lg={6}>
<Select <Select
mode="multiple" mode="multiple"
placeholder="Îáúåêòû" placeholder="Объекты"
allowClear allowClear
style={{ width: '100%' }} style={{ width: '100%' }}
value={objectIds} value={objectIds}
@ -251,7 +251,7 @@ export function HomePage() {
<Col xs={24} sm={12} lg={6}> <Col xs={24} sm={12} lg={6}>
<Select <Select
mode="multiple" mode="multiple"
placeholder="Òåãè" placeholder="Теги"
allowClear allowClear
style={{ width: '100%' }} style={{ width: '100%' }}
value={tagIds} value={tagIds}
@ -263,12 +263,12 @@ export function HomePage() {
</Card> </Card>
<Row gutter={[12, 12]} style={{ marginBottom: 12 }}> <Row gutter={[12, 12]} style={{ marginBottom: 12 }}>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Îáúåêòîâ â ñêîóïå" value={totals.objects} /></Card></Col> <Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Объектов в скоупе" value={totals.objects} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Óñòðîéñòâ" value={totals.devices} /></Card></Col> <Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Устройств" value={totals.devices} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Èíöèäåíòíûõ îáúåêòîâ" value={incidentObjects.length} /></Card></Col> <Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Инцидентных объектов" value={incidentObjects.length} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Ñèãíàëîâ ïðîáëåì" value={signalCount} /></Card></Col> <Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Сигналов проблем" value={signalCount} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Ïðîñðî÷åííûå îáúåêòû" value={stalePingObjects.length} /></Card></Col> <Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Просроченные объекты" value={stalePingObjects.length} /></Card></Col>
<Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Ñðåäíèé health" value={avgHealth ?? '—'} /></Card></Col> <Col xs={12} md={8} lg={4}><Card size="small" loading={isLoading}><Statistic title="Средний health" value={avgHealth ?? '—'} /></Card></Col>
</Row> </Row>
{monitorStatus?.last_error ? <Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} /> : null} {monitorStatus?.last_error ? <Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} /> : null}
@ -276,9 +276,9 @@ export function HomePage() {
<Row gutter={[12, 12]}> <Row gutter={[12, 12]}>
<Col xs={24} xl={16}> <Col xs={24} xl={16}>
<Space direction="vertical" size={12} style={{ width: '100%' }}> <Space direction="vertical" size={12} style={{ width: '100%' }}>
<Card title="Îáúåêòû ñ èíöèäåíòàìè" size="small" style={{ borderRadius: 12 }}> <Card title="Объекты с инцидентами" size="small" style={{ borderRadius: 12 }}>
{incidentObjects.length === 0 ? ( {incidentObjects.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Êðèòè÷íûõ îáúåêòîâ íåò" /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Критичных объектов нет" />
) : ( ) : (
<List <List
dataSource={incidentObjects.slice(0, 12)} dataSource={incidentObjects.slice(0, 12)}
@ -293,7 +293,7 @@ export function HomePage() {
<List.Item <List.Item
actions={[ actions={[
<Button key="open" type="link" onClick={() => navigate(`/home/objects/${item.object_id}${scopeToSearch(scope)}`)}> <Button key="open" type="link" onClick={() => navigate(`/home/objects/${item.object_id}${scopeToSearch(scope)}`)}>
Îòêðûòü Открыть
</Button>, </Button>,
]} ]}
> >
@ -301,17 +301,17 @@ export function HomePage() {
title={ title={
<Space wrap size={8}> <Space wrap size={8}>
<Typography.Text strong>{item.object_name}</Typography.Text> <Typography.Text strong>{item.object_name}</Typography.Text>
<Tag>{item.city ?? '—'}</Tag> <Tag>{item.city ?? '—'}</Tag>
<Tag color="blue">{item.client ?? '—'}</Tag> <Tag color="blue">{item.client ?? '—'}</Tag>
<Tag color={healthColor(item.health_score)}>health {item.health_score ?? '—'}</Tag> <Tag color={healthColor(item.health_score)}>health {item.health_score ?? '—'}</Tag>
</Space> </Space>
} }
description={ description={
<Space split={<span></span>} wrap> <Space split={<span></span>} wrap>
<span>offline: {offline}</span> <span>offline: {offline}</span>
<span>unknown: {unknown}</span> <span>unknown: {unknown}</span>
<span>disk warn/failed: {diskWarn}/{diskFailed}</span> <span>disk warn/failed: {diskWarn}/{diskFailed}</span>
<span>óñòðîéñòâ: {item.device_count}</span> <span>устройств: {item.device_count}</span>
<span>age ping: {formatAge(ping?.max_age_sec)}</span> <span>age ping: {formatAge(ping?.max_age_sec)}</span>
</Space> </Space>
} }
@ -323,15 +323,15 @@ export function HomePage() {
)} )}
</Card> </Card>
<Card title="Îáúåêòû ñ íåñâåæèìè äàííûìè ìîíèòîðèíãà" size="small" style={{ borderRadius: 12 }}> <Card title="Объекты с несвежими данными мониторинга" size="small" style={{ borderRadius: 12 }}>
{stalePingObjects.length === 0 ? ( {stalePingObjects.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Âñå îáúåêòû îáíîâëÿþòñÿ â ïðåäåëàõ SLA" /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Все объекты обновляются в пределах SLA" />
) : ( ) : (
<List <List
size="small" size="small"
dataSource={stalePingObjects} dataSource={stalePingObjects}
renderItem={(item) => ( renderItem={(item) => (
<List.Item actions={[<Button key="open" type="link" onClick={() => navigate(`/home/objects/${item.obj.object_id}${scopeToSearch(scope)}`)}>Îòêðûòü</Button>]}> <List.Item actions={[<Button key="open" type="link" onClick={() => navigate(`/home/objects/${item.obj.object_id}${scopeToSearch(scope)}`)}>Открыть</Button>]}>
<Space direction="vertical" size={0} style={{ width: '100%' }}> <Space direction="vertical" size={0} style={{ width: '100%' }}>
<Row justify="space-between"> <Row justify="space-between">
<Typography.Text>{item.obj.object_name}</Typography.Text> <Typography.Text>{item.obj.object_name}</Typography.Text>
@ -351,28 +351,28 @@ export function HomePage() {
<Col xs={24} xl={8}> <Col xs={24} xl={8}>
<Space direction="vertical" size={12} style={{ width: '100%' }}> <Space direction="vertical" size={12} style={{ width: '100%' }}>
<Card title="SLA è ïîêðûòèå" size="small" style={{ borderRadius: 12 }}> <Card title="SLA и покрытие" size="small" style={{ borderRadius: 12 }}>
<Space direction="vertical" size={10} style={{ width: '100%' }}> <Space direction="vertical" size={10} style={{ width: '100%' }}>
<div> <div>
<Row justify="space-between"> <Row justify="space-between">
<Typography.Text>Äîñòóïíîñòü (online)</Typography.Text> <Typography.Text>Доступность (online)</Typography.Text>
<Typography.Text strong>{availabilityPct != null ? `${availabilityPct}%` : '—'}</Typography.Text> <Typography.Text strong>{availabilityPct != null ? `${availabilityPct}%` : '—'}</Typography.Text>
</Row> </Row>
<Progress percent={availabilityPct ?? 0} size="small" showInfo={false} strokeColor={availabilityPct != null && availabilityPct < 95 ? '#ff4d4f' : '#52c41a'} /> <Progress percent={availabilityPct ?? 0} size="small" showInfo={false} strokeColor={availabilityPct != null && availabilityPct < 95 ? '#ff4d4f' : '#52c41a'} />
</div> </div>
<div> <div>
<Row justify="space-between"> <Row justify="space-between">
<Typography.Text>Ïîêðûòèå ìîíèòîðèíãîì</Typography.Text> <Typography.Text>Покрытие мониторингом</Typography.Text>
<Typography.Text strong>{coveragePct != null ? `${coveragePct}%` : '—'}</Typography.Text> <Typography.Text strong>{coveragePct != null ? `${coveragePct}%` : '—'}</Typography.Text>
</Row> </Row>
<Progress percent={coveragePct ?? 0} size="small" showInfo={false} strokeColor="#1677ff" /> <Progress percent={coveragePct ?? 0} size="small" showInfo={false} strokeColor="#1677ff" />
</div> </div>
<div> <div>
<Row justify="space-between"> <Row justify="space-between">
<Typography.Text>Ñâåæåñòü äàííûõ</Typography.Text> <Typography.Text>Свежесть данных</Typography.Text>
<Typography.Text strong>{freshnessPct != null ? `${freshnessPct}%` : '—'}</Typography.Text> <Typography.Text strong>{freshnessPct != null ? `${freshnessPct}%` : '—'}</Typography.Text>
</Row> </Row>
<Progress percent={freshnessPct ?? 0} size="small" showInfo={false} strokeColor={freshnessPct != null && freshnessPct < 90 ? '#faad14' : '#52c41a'} /> <Progress percent={freshnessPct ?? 0} size="small" showInfo={false} strokeColor={freshnessPct != null && freshnessPct < 90 ? '#faad14' : '#52c41a'} />
</div> </div>
@ -381,16 +381,16 @@ export function HomePage() {
<Tag color="green">online: {onlineTotal}</Tag> <Tag color="green">online: {onlineTotal}</Tag>
<Tag color="red">offline: {offlineTotal}</Tag> <Tag color="red">offline: {offlineTotal}</Tag>
<Tag color="orange">unknown: {unknownTotal}</Tag> <Tag color="orange">unknown: {unknownTotal}</Tag>
<Tag>ïî ping: {monitoredTotal}</Tag> <Tag>по ping: {monitoredTotal}</Tag>
</Space> </Space>
</Space> </Space>
</Card> </Card>
<Card title="Ñîñòîÿíèå öèêëà ìîíèòîðèíãà" size="small" style={{ borderRadius: 12 }}> <Card title="Состояние цикла мониторинга" size="small" style={{ borderRadius: 12 }}>
<Space direction="vertical" size={8} style={{ width: '100%' }}> <Space direction="vertical" size={8} style={{ width: '100%' }}>
<Row justify="space-between"><Typography.Text>Ñòàòóñ</Typography.Text><Tag color={runStatusColor(monitorStatus?.status)}>{monitorStatus?.status ?? '-'}</Tag></Row> <Row justify="space-between"><Typography.Text>Статус</Typography.Text><Tag color={runStatusColor(monitorStatus?.status)}>{monitorStatus?.status ?? '-'}</Tag></Row>
<Row justify="space-between"><Typography.Text>Îáúåêòû â ìîíèòîðèíãå</Typography.Text><Typography.Text strong>{summary?.included_count ?? totals.allowed_objects}</Typography.Text></Row> <Row justify="space-between"><Typography.Text>Объекты в мониторинге</Typography.Text><Typography.Text strong>{summary?.included_count ?? totals.allowed_objects}</Typography.Text></Row>
<Row justify="space-between"><Typography.Text>Ïîñëåäíèé óñïåõ</Typography.Text><Typography.Text strong>{monitorStatus?.last_success_at ? dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss') : '—'}</Typography.Text></Row> <Row justify="space-between"><Typography.Text>Последний успех</Typography.Text><Typography.Text strong>{monitorStatus?.last_success_at ? dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss') : '—'}</Typography.Text></Row>
{lastCounts ? ( {lastCounts ? (
<Space direction="vertical" size={2} style={{ width: '100%' }}> <Space direction="vertical" size={2} style={{ width: '100%' }}>
@ -405,7 +405,7 @@ export function HomePage() {
<Card title="Action queue" size="small" style={{ borderRadius: 12 }}> <Card title="Action queue" size="small" style={{ borderRadius: 12 }}>
{actionQueue.length === 0 ? ( {actionQueue.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Êðèòè÷íûõ äåéñòâèé ñåé÷àñ íåò" /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Критичных действий сейчас нет" />
) : ( ) : (
<List <List
size="small" size="small"
@ -414,7 +414,7 @@ export function HomePage() {
<List.Item <List.Item
actions={[ actions={[
<Button key="open" type="link" onClick={() => navigate(`/home/objects/${row.obj.object_id}${scopeToSearch(scope)}`)}> <Button key="open" type="link" onClick={() => navigate(`/home/objects/${row.obj.object_id}${scopeToSearch(scope)}`)}>
Îòêðûòü Открыть
</Button>, </Button>,
]} ]}
> >
@ -427,7 +427,7 @@ export function HomePage() {
</Col> </Col>
</Row> </Row>
<Typography.Text type="secondary" style={{ fontSize: 12 }}> <Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.reasons.join(' • ')} {row.reasons.join(' • ')}
</Typography.Text> </Typography.Text>
</List.Item> </List.Item>
)} )}
@ -440,3 +440,4 @@ export function HomePage() {
</div> </div>
) )
} }