Files
cloudflare-domain-manager/apps/web/src/components/reui-kit/service-failover-panel.tsx
T
DenozordecandCursor f5d97e463a
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 8s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m2s
CD / quality (push) Successful in 1m14s
CD / publish (push) Successful in 2m3s
fix(services): показывать живой health и историю выхода из пула
Таблица IP брала hysteresis unknown вместо последней пробы. Failover теперь показывает выход и возврат в пул, а не только DNS add.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 16:43:22 +07:00

120 lines
3.7 KiB
TypeScript

import { UnplugIcon } from 'lucide-react'
import { FailoverTimeline } from '@/components/failover-timeline'
import {
mergeFailoverHistory,
toFailoverEvents,
type FailoverBindingPool,
type FailoverHealthInput,
} from '@/lib/failover-events'
import {
latestHealthByIp,
resolveIpDisplayHealth,
type HealthLogProbe,
type HealthLogStatus,
} from '@/lib/health-log'
import type { FailoverLogEntry } from '@/lib/schemas'
import { Badge } from '@/components/reui/badge'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import {
Alert,
AlertDescription,
AlertTitle,
} from '@/components/reui/alert'
function failoverCountLabel(count: number): string {
const mod10 = count % 10
const mod100 = count % 100
if (mod10 === 1 && mod100 !== 11) return `${count} адрес Down`
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) {
return `${count} адреса Down`
}
return `${count} адресов Down`
}
/**
* Failover — текущие Down + кто вышел из пула и кто вернулся.
* Preview: https://reui.io/preview/base/components/c-timeline-10
* Preview: https://reui.io/preview/base/empty-state-12
* Docs: https://reui.io/docs/components/base/frame
* Docs: https://reui.io/docs/components/base/timeline
* Docs: https://reui.io/docs/components/base/badge
* Docs: https://reui.io/docs/components/base/alert
*/
export function ServiceFailoverPanel({
ipHealth,
bindings,
history,
probes = [],
}: {
ipHealth: readonly FailoverHealthInput[]
bindings: readonly FailoverBindingPool[]
history: readonly FailoverLogEntry[]
probes?: readonly HealthLogProbe[]
}) {
const liveByIp = latestHealthByIp(probes)
const overlayHealth = ipHealth.map((row) => {
const live = liveByIp.get(row.ip)
return {
...row,
status: resolveIpDisplayHealth(
row.status as HealthLogStatus,
live?.status,
),
last_error:
live && live.status !== 'unknown' ? live.last_error : row.last_error,
last_checked_at:
live && live.status !== 'unknown'
? live.last_checked_at
: row.last_checked_at,
}
})
const events = toFailoverEvents(overlayHealth, bindings)
const mergedHistory = mergeFailoverHistory(history, probes, bindings)
const removedCount = events.filter((event) => event.kind === 'removed').length
return (
<Frame stacked spacing="sm" className="min-w-0 w-full">
<FramePanel className="flex flex-col gap-3">
<FrameHeader className="gap-1 px-0 py-0">
<FrameTitle className="flex flex-wrap items-center gap-2">
Failover
{events.length > 0 ? (
<Badge variant="destructive-light" size="xs" radius="full">
{events.length}
</Badge>
) : (
<Badge variant="success-light" size="xs" radius="full">
OK
</Badge>
)}
</FrameTitle>
<FrameDescription>
Текущие Down и история: кто вышел из пула и кто вернулся
</FrameDescription>
</FrameHeader>
{events.length > 0 ? (
<Alert variant="destructive">
<UnplugIcon aria-hidden="true" />
<AlertTitle>{failoverCountLabel(events.length)}</AlertTitle>
<AlertDescription>
{removedCount > 0
? 'Сняты с FQDN или остались last-resort'
: 'Остались в A-записях как last-resort'}
</AlertDescription>
</Alert>
) : null}
<FailoverTimeline events={events} history={mergedHistory} />
</FramePanel>
</Frame>
)
}