+
+ {badge}
+
+ ) : undefined
+ }
+ >
+
{total === 0 ? (
Нет данных
) : (
<>
-
-
+
{slices.map((slice) => {
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
return (
- -
+
-
>
)}
- {badge ? (
-
- {badge}
-
- ) : null}
)
diff --git a/apps/web/src/lib/metrics/readiness-breakdown.test.ts b/apps/web/src/lib/metrics/readiness-breakdown.test.ts
new file mode 100644
index 0000000..a585f56
--- /dev/null
+++ b/apps/web/src/lib/metrics/readiness-breakdown.test.ts
@@ -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')
+ })
+})
diff --git a/apps/web/src/lib/metrics/readiness-breakdown.ts b/apps/web/src/lib/metrics/readiness-breakdown.ts
index ddf0fad..583da54 100644
--- a/apps/web/src/lib/metrics/readiness-breakdown.ts
+++ b/apps/web/src/lib/metrics/readiness-breakdown.ts
@@ -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)',
})
}
diff --git a/apps/web/src/lib/ui-labels.ts b/apps/web/src/lib/ui-labels.ts
index b03759e..014e83d 100644
--- a/apps/web/src/lib/ui-labels.ts
+++ b/apps/web/src/lib/ui-labels.ts
@@ -117,6 +117,8 @@ export function readyCheckRu(key: string): string {
return 'Хранилище'
case 'jobs':
return 'Очередь задач'
+ case 'store_backend':
+ return 'Бэкенд хранилища'
default:
return key
}
diff --git a/apps/web/src/queries/monitoring.ts b/apps/web/src/queries/monitoring.ts
index 733cccf..f2928bc 100644
--- a/apps/web/src/queries/monitoring.ts
+++ b/apps/web/src/queries/monitoring.ts
@@ -9,7 +9,8 @@ export interface HealthStatus {
export interface ReadyStatus {
status?: string
- checks?: Record
+ /** Backend: string ("ok"|"memory"|"unavailable"), boolean, or { ok, error }. */
+ checks?: Record
}
export interface VersionInfo {
diff --git a/apps/web/src/routes/_auth/monitoring.tsx b/apps/web/src/routes/_auth/monitoring.tsx
index 591adb9..0d2d28c 100644
--- a/apps/web/src/routes/_auth/monitoring.tsx
+++ b/apps/web/src/routes/_auth/monitoring.tsx
@@ -81,7 +81,7 @@ function MonitoringComponent() {
.slice(0, 5)
return (
-
+
-
+
{analyticsLoading ? (
) : (
-
+
)}
-
+
}
description="GET /v1/bird/status"
- contentClassName="py-4"
+ className="h-full"
+ contentClassName="px-5 py-4"
>
- }
- onRetry={() => birdQ.refetch()}
- >
- {(bird) => }
-
+ }
+ onRetry={() => birdQ.refetch()}
+ >
+ {(bird) => }
+
-
-
+
+
{failedJobs.length > 0 ? (
-
-
+
+
{failedJobs.map((job) => (
-
@@ -218,28 +219,29 @@ function MonitoringComponent() {
}
description="Краткая шпаргалка для первичной диагностики"
- contentClassName="py-4"
+ className="h-full"
+ contentClassName="px-5 py-4"
>
-
- -
- API недоступен. Если{' '}
-
/v1/health возвращает ошибку — проверьте процесс API и
- его логи.
-
- -
- Готовность не «Готов». Сначала{' '}
-
postgres, затем store{' '}
- и jobs в проверках.
-
- -
- Низкий ratio BGP. Проверьте{' '}
-
/v1/bird/status, затем состояние пиров в Сети.
-
- -
- Ошибки задач. Откройте Операции и
- проверьте последние неуспешные задачи.
-
-
+
+ -
+ API недоступен. Если{' '}
+
/v1/health возвращает ошибку — проверьте процесс API и
+ его логи.
+
+ -
+ Готовность не «Готов». Сначала{' '}
+
postgres, затем store{' '}
+ и jobs в проверках.
+
+ -
+ Низкий ratio BGP. Проверьте{' '}
+
/v1/bird/status, затем состояние пиров в Сети.
+
+ -
+ Ошибки задач. Откройте Операции и
+ проверьте последние неуспешные задачи.
+
+
@@ -282,7 +284,7 @@ function BirdSummary({ bird }: { bird: import('@/types/api').BirdStatus }) {
? Math.round((bird.bgp_established / bird.bgp_sessions_total) * 100)
: null
return (
-