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
Таблица IP брала hysteresis unknown вместо последней пробы. Failover теперь показывает выход и возврат в пул, а не только DNS add. Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
3.7 KiB
TypeScript
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>
|
|
)
|
|
}
|