fix(web): parse string readiness checks on monitoring System tab
Исправляет ложные «Ошибки проверок»: GET /v1/ready отдаёт строки ok/memory, а не boolean. Уплотнён ReUI Frame/donut layout на вкладке Система. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
isReadyCheckOk,
|
||||
isSystemReady,
|
||||
readinessBreakdown,
|
||||
readyCheckStatusLabel,
|
||||
} from '@/lib/metrics/readiness-breakdown'
|
||||
|
||||
describe('isSystemReady', () => {
|
||||
it('accepts ready and ok', () => {
|
||||
expect(isSystemReady('ready')).toBe(true)
|
||||
expect(isSystemReady('ok')).toBe(true)
|
||||
expect(isSystemReady('READY')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects not_ready and empty', () => {
|
||||
expect(isSystemReady('not_ready')).toBe(false)
|
||||
expect(isSystemReady(undefined)).toBe(false)
|
||||
expect(isSystemReady('')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isReadyCheckOk', () => {
|
||||
it('parses backend string checks as ok', () => {
|
||||
expect(isReadyCheckOk('ok')).toBe(true)
|
||||
expect(isReadyCheckOk('memory')).toBe(true)
|
||||
expect(isReadyCheckOk('unavailable')).toBe(false)
|
||||
})
|
||||
|
||||
it('parses boolean and object forms', () => {
|
||||
expect(isReadyCheckOk(true)).toBe(true)
|
||||
expect(isReadyCheckOk(false)).toBe(false)
|
||||
expect(isReadyCheckOk({ ok: true })).toBe(true)
|
||||
expect(isReadyCheckOk({ ok: false, error: 'down' })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readyCheckStatusLabel', () => {
|
||||
it('labels memory and failures', () => {
|
||||
expect(readyCheckStatusLabel('memory', true)).toBe('Memory')
|
||||
expect(readyCheckStatusLabel('ok', true)).toBe('В норме')
|
||||
expect(readyCheckStatusLabel('unavailable', false)).toBe('Недоступно')
|
||||
})
|
||||
})
|
||||
|
||||
describe('readinessBreakdown', () => {
|
||||
it('counts healthy handleReady payload without false failures', () => {
|
||||
const slices = readinessBreakdown(
|
||||
{
|
||||
status: 'ready',
|
||||
checks: { store: 'ok', jobs: 'memory', postgres: 'ok' },
|
||||
},
|
||||
true,
|
||||
)
|
||||
expect(slices.find((s) => s.key === 'checks-fail')).toBeUndefined()
|
||||
expect(slices.find((s) => s.key === 'checks-ok')?.count).toBe(3)
|
||||
expect(slices.find((s) => s.key === 'health')?.count).toBe(1)
|
||||
})
|
||||
|
||||
it('counts unavailable checks as failures', () => {
|
||||
const slices = readinessBreakdown(
|
||||
{
|
||||
status: 'not_ready',
|
||||
checks: { store: 'unavailable', jobs: 'memory' },
|
||||
},
|
||||
true,
|
||||
)
|
||||
expect(slices.find((s) => s.key === 'checks-fail')?.count).toBe(1)
|
||||
expect(slices.find((s) => s.key === 'checks-ok')?.count).toBe(1)
|
||||
})
|
||||
|
||||
it('returns API down slice when health fails', () => {
|
||||
const slices = readinessBreakdown({ status: 'ready', checks: { store: 'ok' } }, false)
|
||||
expect(slices).toHaveLength(1)
|
||||
expect(slices[0]?.key).toBe('health-fail')
|
||||
})
|
||||
})
|
||||
@@ -2,12 +2,65 @@ import type { ReadyStatus } from '@/queries/monitoring'
|
||||
|
||||
import type { BreakdownSlice } from './types'
|
||||
|
||||
function checkOk(value: boolean | { ok?: boolean; error?: string } | undefined): boolean {
|
||||
/** Значение check из GET /v1/ready (строка, boolean или объект). */
|
||||
export type ReadyCheckValue = boolean | string | { ok?: boolean; error?: string } | null | undefined
|
||||
|
||||
const OK_STRINGS = new Set(['ok', 'ready', 'memory', 'true', 'healthy', 'up'])
|
||||
const FAIL_STRINGS = new Set([
|
||||
'unavailable',
|
||||
'error',
|
||||
'not_ready',
|
||||
'failed',
|
||||
'down',
|
||||
'false',
|
||||
'unhealthy',
|
||||
])
|
||||
|
||||
/** Top-level status GET /v1/ready: API отдаёт `ready`, не `ok`. */
|
||||
export function isSystemReady(status: string | null | undefined): boolean {
|
||||
if (!status) return false
|
||||
const normalized = status.trim().toLowerCase()
|
||||
return normalized === 'ready' || normalized === 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* Интерпретация check value по контракту handleReady:
|
||||
* store/postgres → "ok" | "unavailable"; jobs → "memory"; store_backend → "memory".
|
||||
*/
|
||||
export function isReadyCheckOk(value: ReadyCheckValue): boolean {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toLowerCase()
|
||||
if (OK_STRINGS.has(normalized)) return true
|
||||
if (FAIL_STRINGS.has(normalized)) return false
|
||||
// неизвестная непустая строка — считать OK (информативный статус бэкенда)
|
||||
return normalized.length > 0
|
||||
}
|
||||
if (value && typeof value === 'object') return value.ok === true
|
||||
return false
|
||||
}
|
||||
|
||||
/** Человекочитаемый статус check для UI. */
|
||||
export function readyCheckStatusLabel(value: ReadyCheckValue, ok: boolean): string {
|
||||
if (!ok) {
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const n = value.trim().toLowerCase()
|
||||
if (n === 'unavailable') return 'Недоступно'
|
||||
if (n === 'not_ready') return 'Не готов'
|
||||
return value
|
||||
}
|
||||
if (value && typeof value === 'object' && value.error) return value.error
|
||||
return 'Ошибка'
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const n = value.trim().toLowerCase()
|
||||
if (n === 'memory') return 'Memory'
|
||||
if (n === 'ok' || n === 'ready' || n === 'healthy' || n === 'true') return 'В норме'
|
||||
if (n) return value
|
||||
}
|
||||
return 'В норме'
|
||||
}
|
||||
|
||||
export function readinessBreakdown(
|
||||
ready: ReadyStatus | null | undefined,
|
||||
healthOk: boolean,
|
||||
@@ -28,7 +81,7 @@ export function readinessBreakdown(
|
||||
let failCount = 0
|
||||
|
||||
for (const value of Object.values(checks)) {
|
||||
if (checkOk(value)) okCount += 1
|
||||
if (isReadyCheckOk(value)) okCount += 1
|
||||
else failCount += 1
|
||||
}
|
||||
|
||||
@@ -61,9 +114,11 @@ export function readinessBreakdown(
|
||||
if (slices.length === 1 && okCount === 0 && failCount === 0) {
|
||||
slices.push({
|
||||
key: 'ready',
|
||||
label: ready?.status === 'ok' ? 'Готов' : 'Ожидает готовности',
|
||||
label: isSystemReady(ready?.status) ? 'Готов' : 'Не готов',
|
||||
count: 1,
|
||||
color: 'var(--color-chart-4)',
|
||||
color: isSystemReady(ready?.status)
|
||||
? 'var(--color-chart-1)'
|
||||
: 'var(--color-warning)',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,8 @@ export function readyCheckRu(key: string): string {
|
||||
return 'Хранилище'
|
||||
case 'jobs':
|
||||
return 'Очередь задач'
|
||||
case 'store_backend':
|
||||
return 'Бэкенд хранилища'
|
||||
default:
|
||||
return key
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user