diff --git a/web/src/lib/monitoring/status.ts b/web/src/lib/monitoring/status.ts new file mode 100644 index 0000000..cc7b025 --- /dev/null +++ b/web/src/lib/monitoring/status.ts @@ -0,0 +1,169 @@ +/** Pure-helpers для страницы /monitoring. */ + +export type OverallStatus = 'ok' | 'warn' | 'error' | 'unknown'; + +export type JobsKpi = { running: number; failed: number; total: number }; + +export type ReadyStatus = { status: string; checks?: Record }; + +export type HealthStatus = { ok: boolean; status?: string; error?: string }; + +export type CheckBadge = { + label: string; + variant: 'default' | 'secondary' | 'destructive' | 'outline'; + class?: string; + hint?: string; +}; + +export function toErrorMessage(error: unknown): string { + if (error instanceof Error && error.message.trim() !== '') return error.message; + return 'Ошибка запроса'; +} + +export function summarizeJobsStatuses(items: Array<{ status?: string }>): JobsKpi { + let running = 0; + let failed = 0; + for (const row of items) { + const status = String(row.status ?? '').toLowerCase(); + if (status === 'queued' || status === 'running' || status === 'cancel_requested') { + running += 1; + } + if (status === 'failed' || status === 'error' || status === 'canceled') { + failed += 1; + } + } + return { running, failed, total: items.length }; +} + +export function deriveOverallStatus(input: { + health: HealthStatus | null; + ready: ReadyStatus | null; + jobs: JobsKpi | null; + birdConfigured: boolean; + birdHealthy: boolean | null; +}): OverallStatus { + const { health, ready, jobs, birdConfigured, birdHealthy } = input; + if (health === null && ready === null && jobs === null) return 'unknown'; + if (!health?.ok) return 'error'; + if (ready !== null && ready.status !== 'ready') return 'error'; + if (jobs !== null && jobs.failed > 0) return 'warn'; + if (birdConfigured && birdHealthy === false) return 'warn'; + return 'ok'; +} + +export function overallStatusLabel(status: OverallStatus): string { + switch (status) { + case 'ok': + return 'В норме'; + case 'warn': + return 'Внимание'; + case 'error': + return 'Ошибка'; + default: + return 'Нет данных'; + } +} + +export function overallStatusHint( + status: OverallStatus, + input: { + healthOk: boolean | undefined; + jobsFailed: number; + } +): string { + if (status === 'unknown') return 'Нет данных. Запустите обновление.'; + if (status === 'error') { + if (!input.healthOk) return 'Проверьте доступность API и логи сервиса.'; + return 'Readiness не в норме: проверьте postgres/store/jobs.'; + } + if (status === 'warn') { + if (input.jobsFailed > 0) return 'Есть ошибки в задачах: откройте операции и последние jobs.'; + return 'Проверьте BGP-сессии и вывод birdc.'; + } + return 'Критичных отклонений не обнаружено.'; +} + +export function overallBadgeVariant(status: OverallStatus): CheckBadge['variant'] { + if (status === 'ok') return 'default'; + if (status === 'warn') return 'secondary'; + if (status === 'error') return 'destructive'; + return 'outline'; +} + +export function overallBadgeClass(status: OverallStatus): string | undefined { + if (status === 'ok') return 'border-success/30 bg-success/15 text-success'; + if (status === 'warn') return 'border-warning/30 bg-warning/15 text-warning'; + return undefined; +} + +/** Нормализация значений ready.checks и health/readiness в badge. */ +export function checkStatusBadge(value: unknown): CheckBadge { + const raw = String(value ?? '').trim(); + const lower = raw.toLowerCase(); + + if (lower === 'ok' || lower === 'ready' || lower === 'true' || lower === 'up') { + return { + label: 'OK', + variant: 'default', + class: 'border-success/30 bg-success/15 text-success' + }; + } + if (lower === 'memory') { + return { + label: 'In-memory', + variant: 'secondary', + hint: 'Очередь задач в памяти процесса, не shared между воркерами.' + }; + } + if ( + lower === 'failed' || + lower === 'false' || + lower === 'error' || + lower === 'down' || + lower === 'unavailable' + ) { + return { label: 'Ошибка', variant: 'destructive' }; + } + if (raw === '') { + return { label: '—', variant: 'outline' }; + } + return { label: raw, variant: 'outline' }; +} + +/** Человекочитаемое имя проверки readiness. */ +export function checkDisplayName(key: string): string { + switch (key) { + case 'postgres': + return 'PostgreSQL'; + case 'store': + return 'Хранилище'; + case 'jobs': + return 'Очередь задач'; + default: + return key; + } +} + +export function livenessBadge(health: HealthStatus | null): CheckBadge { + if (health === null) return { label: '—', variant: 'outline' }; + if (health.ok) { + return { + label: 'В норме', + variant: 'default', + class: 'border-success/30 bg-success/15 text-success' + }; + } + return { label: 'Недоступен', variant: 'destructive' }; +} + +export function readinessBadge(ready: ReadyStatus | null): CheckBadge { + if (ready === null) return { label: '—', variant: 'outline' }; + if (ready.status === 'ready') { + return { + label: 'Готов', + variant: 'default', + class: 'border-success/30 bg-success/15 text-success' + }; + } + return { label: ready.status || 'Не готов', variant: 'destructive' }; +} diff --git a/web/src/routes/monitoring/+page.svelte b/web/src/routes/monitoring/+page.svelte index a4f0ff2..2a9e21b 100644 --- a/web/src/routes/monitoring/+page.svelte +++ b/web/src/routes/monitoring/+page.svelte @@ -1,21 +1,54 @@