fix(services): оформить failover timeline в стиле страницы сервиса
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 9s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 53s
CD / quality (push) Successful in 1m6s
CD / publish (push) Successful in 1m39s

Frame stacked как у мониторинга, Alert и semantic timeline вместо плоского степпера.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-20 14:31:30 +07:00
co-authored by Cursor
parent ba4e04a224
commit 87b0f1a894
6 changed files with 269 additions and 52 deletions
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import {
isFailoverEventStatus,
toFailoverEvents,
type FailoverNodeInput,
} from '@/lib/failover-events'
function node(
overrides: Partial<FailoverNodeInput> & Pick<FailoverNodeInput, 'address'>,
): FailoverNodeInput {
return {
id: overrides.id ?? overrides.address,
health_status: 'healthy',
consecutive_failures: 0,
last_failure_reason: null,
last_check_at: null,
...overrides,
}
}
describe('toFailoverEvents', () => {
it('оставляет только unhealthy / down / checking', () => {
const events = toFailoverEvents([
node({ address: '10.0.0.1', health_status: 'healthy' }),
node({
address: '130.49.213.153',
health_status: 'unhealthy',
consecutive_failures: 9,
last_failure_reason: 'fetch failed',
last_check_at: '2026-08-20 07:00:00',
}),
node({ address: '10.0.0.3', health_status: 'checking' }),
node({ address: '10.0.0.4', health_status: 'disabled' }),
])
expect(events.map((event) => event.address)).toEqual([
'130.49.213.153',
'10.0.0.3',
])
expect(events[0]).toMatchObject({
consecutiveFailures: 9,
lastFailureReason: 'fetch failed',
lastCheckAt: '2026-08-20 07:00:00',
status: 'unhealthy',
})
})
it('не считает healthy failover-событием', () => {
expect(isFailoverEventStatus('healthy')).toBe(false)
expect(isFailoverEventStatus('unhealthy')).toBe(true)
})
})
+36
View File
@@ -0,0 +1,36 @@
export interface FailoverEvent {
id: string
address: string
status: string
consecutiveFailures: number
lastFailureReason: string | null
lastCheckAt?: string | null
}
export interface FailoverNodeInput {
id?: number | string
address: string
health_status: string
consecutive_failures: number
last_failure_reason: string | null
last_check_at?: string | null
}
const FAILOVER_STATUSES = new Set(['unhealthy', 'down', 'checking'])
export function isFailoverEventStatus(status: string): boolean {
return FAILOVER_STATUSES.has(status)
}
export function toFailoverEvents(
nodes: readonly FailoverNodeInput[],
): FailoverEvent[] {
return nodes.filter((node) => isFailoverEventStatus(node.health_status)).map((node) => ({
id: String(node.id ?? node.address),
address: node.address,
status: node.health_status,
consecutiveFailures: node.consecutive_failures,
lastFailureReason: node.last_failure_reason,
lastCheckAt: node.last_check_at ?? null,
}))
}