krona-front/src/features/control-center/pages/OverviewPage.tsx

582 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Building2,
Users,
CreditCard,
Smartphone,
Server,
ShieldAlert,
ArrowUpRight,
Activity,
AlertTriangle,
CheckCircle2,
Clock,
Layers,
HelpCircle,
Database,
} from 'lucide-react';
import { useKccStore, useOperatorRole } from '../store/kccStore';
import { MoneyDisplay } from '../components/MoneyDisplay';
import { StatusBadge } from '../components/StatusBadge';
import { HeartbeatTimeline } from '../components/HeartbeatTimeline';
import { RU_ROLES, RU_INCIDENT_SEVERITY, RU_INCIDENT_STATUS } from '../locales/ru';
type OverviewTab = 'attention' | 'metrics' | 'infrastructure';
export const OverviewPage: React.FC = () => {
const navigate = useNavigate();
const { role, operator } = useOperatorRole();
const organizations = useKccStore((state) => state.organizations);
const subscriptions = useKccStore((state) => state.subscriptions);
const installations = useKccStore((state) => state.installations);
const instances = useKccStore((state) => state.instances);
const servers = useKccStore((state) => state.servers);
const incidents = useKccStore((state) => state.incidents);
const openInspector = useKccStore((state) => state.openInspector);
const serverMonitorOutage = useKccStore((state) => state.serverMonitorOutage);
// Aggregates
const totalOrgs = organizations.length;
const activeOrgs = organizations.filter((o) => o.platformStatus === 'active').length;
const trialOrgs = organizations.filter((o) => o.platformStatus === 'trial').length;
const graceOrgs = organizations.filter((o) => o.platformStatus === 'grace').length;
const suspendedOrgs = organizations.filter((o) => o.platformStatus === 'suspended').length;
const totalUsers = organizations.reduce((sum, o) => sum + o.userCount, 0);
const totalBranches = organizations.reduce((sum, o) => sum + o.branchCount, 0);
const totalMrrMinorUnits = organizations.reduce((sum, o) => sum + o.mrrMinorUnits, 0);
const expiring7d = subscriptions.filter((s) => s.status === 'expiring_7d');
const expiring30d = subscriptions.filter((s) => s.status === 'expiring_30d');
const overdueSubs = subscriptions.filter((s) => s.status === 'overdue');
const webInstalls = installations.filter((i) => i.kind === 'web').length;
const desktopInstalls = installations.filter((i) => i.kind === 'desktop').length;
const androidInstalls = installations.filter((i) => i.kind === 'android').length;
const outdatedAndroid = installations.filter((i) => i.kind === 'android' && i.status === 'outdated').length;
const instancesOffline = instances.filter((i) => i.health === 'offline');
const instancesDegraded = instances.filter((i) => i.health === 'degraded');
const instancesHealthy = instances.filter((i) => i.health === 'healthy');
const serversHealthy = servers.filter((s) => s.status === 'healthy').length;
const serversProblematic = servers.filter((s) => s.status !== 'healthy');
const failedBackups = servers.filter((s) => s.backup.state === 'failed');
const openIncidents = incidents.filter((i) => i.status !== 'resolved');
// Total attention items count
const attentionCount = useMemo(() => {
let count = openIncidents.length;
count += serversProblematic.length;
count += instancesOffline.length;
count += instancesDegraded.length;
count += expiring7d.length;
count += overdueSubs.length;
if (serverMonitorOutage) count += 1;
return count;
}, [openIncidents, serversProblematic, instancesOffline, instancesDegraded, expiring7d, overdueSubs, serverMonitorOutage]);
// Default tab is 'attention' if there are problems, otherwise 'metrics'
const [activeTab, setActiveTab] = useState<OverviewTab>(() => (attentionCount > 0 ? 'attention' : 'metrics'));
return (
<div className="space-y-4 sm:space-y-6" data-testid="overview-page">
{/* Top Headline */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2.5 border-b border-krona-border pb-4">
<div>
<h1 className="text-lg sm:text-xl font-bold text-krona-primary flex items-center gap-2 flex-wrap">
<span>Сводная панель платформы</span>
<span className="text-[11px] px-2.5 py-0.5 rounded-full bg-status-success-soft text-status-success-text border border-status-success-border font-medium">
Контур платформы
</span>
</h1>
<p className="text-xs text-krona-secondary mt-0.5">
Оперативный контроль организаций, лицензий, модульных инстансов и серверных узлов Krona
</p>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="text-krona-tertiary">Оператор:</span>
<span className="font-semibold text-krona-gold bg-krona-gold/10 px-2.5 py-1 rounded-md border border-krona-gold/20">
{operator.name} ({RU_ROLES[role]?.shortLabel || role})
</span>
</div>
</div>
{/* Tab Switcher (Mobile-First Priority Controls) */}
<div className="flex items-center gap-1.5 p-1 bg-krona-surface border border-krona-border rounded-xl max-w-full overflow-x-auto">
<button
type="button"
onClick={() => setActiveTab('attention')}
className={`flex-1 min-h-[44px] min-w-[120px] px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-center gap-2 transition-colors ${
activeTab === 'attention'
? 'bg-krona-workspace text-krona-primary shadow-xs border border-krona-border font-bold'
: 'text-krona-secondary hover:text-krona-primary hover:bg-krona-hover'
}`}
data-testid="tab-attention"
>
<ShieldAlert className={`w-4 h-4 shrink-0 ${attentionCount > 0 ? 'text-status-danger-text' : 'text-status-success-text'}`} />
<span>Требует внимания</span>
{attentionCount > 0 && (
<span className="px-1.5 py-0.2 rounded-full text-[10px] font-mono font-bold bg-status-danger-soft text-status-danger-text border border-status-danger-border">
{attentionCount}
</span>
)}
</button>
<button
type="button"
onClick={() => setActiveTab('metrics')}
className={`flex-1 min-h-[44px] min-w-[110px] px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-center gap-2 transition-colors ${
activeTab === 'metrics'
? 'bg-krona-workspace text-krona-primary shadow-xs border border-krona-border font-bold'
: 'text-krona-secondary hover:text-krona-primary hover:bg-krona-hover'
}`}
data-testid="tab-metrics"
>
<Activity className="w-4 h-4 shrink-0 text-krona-gold" />
<span>Показатели</span>
</button>
<button
type="button"
onClick={() => setActiveTab('infrastructure')}
className={`flex-1 min-h-[44px] min-w-[120px] px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-center gap-2 transition-colors ${
activeTab === 'infrastructure'
? 'bg-krona-workspace text-krona-primary shadow-xs border border-krona-border font-bold'
: 'text-krona-secondary hover:text-krona-primary hover:bg-krona-hover'
}`}
data-testid="tab-infrastructure"
>
<Server className="w-4 h-4 shrink-0 text-krona-gold" />
<span>Инфраструктура</span>
</button>
</div>
{/* ========================================================================= */}
{/* TAB 1: ТРЕБУЕТ ВНИМАНИЯ (CRITICAL / PROBLEMS PRIORITY) */}
{/* ========================================================================= */}
{activeTab === 'attention' && (
<div className="space-y-4 animate-in fade-in duration-150" data-testid="attention-section">
{attentionCount === 0 && (
<div className="p-6 rounded-xl border border-status-success-border bg-status-success-soft text-status-success-text text-center space-y-2">
<CheckCircle2 className="w-10 h-10 mx-auto text-status-success-text" />
<div className="font-bold text-base">Все системы работают в штатном режиме</div>
<p className="text-xs opacity-90 max-w-md mx-auto">
Нет открытых аварий, серверов с деградацией или просроченных подписок, требующих вмешательства оператора.
</p>
</div>
)}
{/* 1. Открытые инциденты платформы */}
{openIncidents.length > 0 && (
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-status-danger-text" />
<span className="font-bold text-sm text-krona-primary">
Открытые инциденты ({openIncidents.length})
</span>
</div>
<button
type="button"
onClick={() => navigate('/control/incidents')}
className="min-h-[44px] px-3 text-xs text-krona-gold hover:underline flex items-center gap-1 font-medium"
>
<span>Все инциденты</span>
<ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
<div className="space-y-2">
{openIncidents.map((inc) => (
<div
key={inc.id}
onClick={() => openInspector({ type: 'incident', id: inc.id, title: inc.title, data: inc })}
className="p-3.5 rounded-lg border border-status-danger-border bg-status-danger-soft cursor-pointer hover:opacity-95 transition-opacity space-y-1.5"
>
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono font-bold bg-status-danger-text text-white px-2 py-0.5 rounded-sm">
{RU_INCIDENT_SEVERITY[inc.severity]?.short || inc.severity}
</span>
<span className="font-bold text-xs text-status-danger-text">{inc.title}</span>
</div>
<span className="text-[11px] font-medium text-status-danger-text">
{RU_INCIDENT_STATUS[inc.status] || inc.status}
</span>
</div>
<p className="text-xs text-krona-secondary line-clamp-2">{inc.impactSummary}</p>
<div className="text-[11px] text-krona-tertiary">
Затронуто: {inc.affectedTenants.length} орг. • Узлы: {inc.affectedNodes.join(', ')}
</div>
</div>
))}
</div>
</div>
)}
{/* 2. Проблемные серверы */}
{serversProblematic.length > 0 && (
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Server className="w-4 h-4 text-status-warning-text" />
<span className="font-bold text-sm text-krona-primary">
Серверы с предупреждениями ({serversProblematic.length})
</span>
</div>
<button
type="button"
onClick={() => navigate('/control/infrastructure')}
className="min-h-[44px] px-3 text-xs text-krona-gold hover:underline flex items-center gap-1 font-medium"
>
<span>Инфраструктура</span>
<ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{serversProblematic.map((srv) => (
<div
key={srv.id}
onClick={() => openInspector({ type: 'server', id: srv.id, title: srv.hostname, data: srv })}
className="p-3.5 rounded-lg border border-krona-border bg-krona-workspace cursor-pointer hover:border-krona-gold/40 transition-colors space-y-2"
>
<div className="flex items-center justify-between">
<div className="font-bold text-xs text-krona-primary font-mono">{srv.hostname}</div>
<StatusBadge status={srv.status} size="sm" />
</div>
<div className="grid grid-cols-3 gap-1 text-[11px] text-krona-secondary font-mono">
<div>CPU: <span className={srv.metrics.cpuPercent > 80 ? 'text-status-danger-text font-bold' : ''}>{srv.metrics.cpuPercent}%</span></div>
<div>RAM: <span className={srv.metrics.ramPercent > 80 ? 'text-status-danger-text font-bold' : ''}>{srv.metrics.ramPercent}%</span></div>
<div>Диск: <span className={srv.metrics.diskPercent > 85 ? 'text-status-danger-text font-bold' : ''}>{srv.metrics.diskPercent}%</span></div>
</div>
<div className="text-[11px] text-krona-tertiary">
Организация: {srv.organizationName}
</div>
</div>
))}
</div>
</div>
)}
{/* 3. Модули с деградацией или сбоем */}
{(instancesOffline.length > 0 || instancesDegraded.length > 0) && (
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Layers className="w-4 h-4 text-status-warning-text" />
<span className="font-bold text-sm text-krona-primary">
Модули с деградацией ({instancesOffline.length + instancesDegraded.length})
</span>
</div>
<button
type="button"
onClick={() => navigate('/control/modules')}
className="min-h-[44px] px-3 text-xs text-krona-gold hover:underline flex items-center gap-1 font-medium"
>
<span>Каталог</span>
<ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
{[...instancesOffline, ...instancesDegraded].map((inst) => (
<div
key={inst.id}
onClick={() => openInspector({ type: 'instance', id: inst.id, title: inst.moduleCode, data: inst })}
className="p-3 rounded-lg border border-krona-border bg-krona-workspace cursor-pointer hover:border-krona-gold/40 transition-colors flex items-center justify-between"
>
<div>
<div className="font-bold text-xs text-krona-primary">{inst.moduleCode} v{inst.version}</div>
<div className="text-[11px] text-krona-tertiary">Организация: {inst.organizationId}</div>
</div>
<StatusBadge status={inst.health} size="sm" />
</div>
))}
</div>
</div>
)}
{/* 4. Истекающие и просроченные подписки */}
{(expiring7d.length > 0 || overdueSubs.length > 0) && (
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CreditCard className="w-4 h-4 text-status-warning-text" />
<span className="font-bold text-sm text-krona-primary">
Истекающие подписки ({expiring7d.length + overdueSubs.length})
</span>
</div>
<button
type="button"
onClick={() => navigate('/control/billing')}
className="min-h-[44px] px-3 text-xs text-krona-gold hover:underline flex items-center gap-1 font-medium"
>
<span>Биллинг</span>
<ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
{[...overdueSubs, ...expiring7d].map((sub) => (
<div
key={sub.id}
onClick={() => openInspector({ type: 'subscription', id: sub.id, title: sub.organizationName, data: sub })}
className="p-3 rounded-lg border border-krona-border bg-krona-workspace cursor-pointer hover:border-krona-gold/40 transition-colors flex items-center justify-between"
>
<div>
<div className="font-bold text-xs text-krona-primary">{sub.organizationName}</div>
<div className="text-[11px] text-krona-tertiary">
Тариф: {sub.planName} • До: {new Date(sub.validUntil).toLocaleDateString('ru-RU')}
</div>
</div>
<div className="text-right">
<MoneyDisplay minorUnits={sub.priceMinorUnits} currency={sub.currency} className="font-bold text-xs text-krona-primary" />
<div className="mt-1"><StatusBadge status={sub.status} size="sm" /></div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* ========================================================================= */}
{/* TAB 2: ПОКАЗАТЕЛИ (KEY METRICS & KPI) */}
{/* ========================================================================= */}
{activeTab === 'metrics' && (
<div className="space-y-4 animate-in fade-in duration-150" data-testid="metrics-section">
{/* 6 Key KPI Cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-6 gap-2.5 sm:gap-3">
{/* Orgs */}
<div
onClick={() => navigate('/control/organizations')}
className="p-3.5 rounded-xl border border-krona-border bg-krona-surface hover:border-krona-gold/40 cursor-pointer transition-colors"
>
<div className="flex items-center justify-between text-krona-tertiary mb-1">
<span className="text-[11px] font-medium">Организации</span>
<Building2 className="w-4 h-4 text-krona-gold" />
</div>
<div className="text-lg sm:text-xl font-bold text-krona-primary">{totalOrgs}</div>
<div className="text-[11px] text-status-success-text mt-0.5">{activeOrgs} активных</div>
</div>
{/* Users */}
<div className="p-3.5 rounded-xl border border-krona-border bg-krona-surface">
<div className="flex items-center justify-between text-krona-tertiary mb-1">
<span className="text-[11px] font-medium">Пользователи</span>
<Users className="w-4 h-4 text-krona-gold" />
</div>
<div className="text-lg sm:text-xl font-bold text-krona-primary">{totalUsers}</div>
<div className="text-[11px] text-krona-secondary mt-0.5">{totalBranches} филиалов</div>
</div>
{/* MRR */}
<div
onClick={() => navigate('/control/billing')}
className="p-3.5 rounded-xl border border-krona-border bg-krona-surface hover:border-krona-gold/40 cursor-pointer transition-colors col-span-2 sm:col-span-1"
>
<div className="flex items-center justify-between text-krona-tertiary mb-1">
<span className="text-[11px] font-medium">Выручка (MRR)</span>
<CreditCard className="w-4 h-4 text-krona-gold" />
</div>
<MoneyDisplay
minorUnits={totalMrrMinorUnits}
currency="RUB"
className="text-lg sm:text-xl font-bold text-krona-gold"
/>
<div className="text-[11px] text-krona-secondary mt-0.5">В месяц</div>
</div>
{/* Subscriptions */}
<div
onClick={() => navigate('/control/billing')}
className="p-3.5 rounded-xl border border-krona-border bg-krona-surface hover:border-krona-gold/40 cursor-pointer transition-colors"
>
<div className="flex items-center justify-between text-krona-tertiary mb-1">
<span className="text-[11px] font-medium">Подписки</span>
<Clock className="w-4 h-4 text-krona-gold" />
</div>
<div className="text-lg sm:text-xl font-bold text-krona-primary">{subscriptions.length}</div>
<div className="text-[11px] text-krona-secondary mt-0.5">{expiring7d.length} истекают</div>
</div>
{/* Installations */}
<div
onClick={() => navigate('/control/installations')}
className="p-3.5 rounded-xl border border-krona-border bg-krona-surface hover:border-krona-gold/40 cursor-pointer transition-colors"
>
<div className="flex items-center justify-between text-krona-tertiary mb-1">
<span className="text-[11px] font-medium">Установки</span>
<Smartphone className="w-4 h-4 text-krona-gold" />
</div>
<div className="text-lg sm:text-xl font-bold text-krona-primary">{installations.length}</div>
<div className="text-[11px] text-krona-secondary mt-0.5">Android: {androidInstalls}</div>
</div>
{/* Servers */}
<div
onClick={() => navigate('/control/infrastructure')}
className="p-3.5 rounded-xl border border-krona-border bg-krona-surface hover:border-krona-gold/40 cursor-pointer transition-colors"
>
<div className="flex items-center justify-between text-krona-tertiary mb-1">
<span className="text-[11px] font-medium">Серверы</span>
<Server className="w-4 h-4 text-krona-gold" />
</div>
<div className="text-lg sm:text-xl font-bold text-krona-primary">{servers.length}</div>
<div className="text-[11px] text-status-success-text mt-0.5">{serversHealthy} в норме</div>
</div>
</div>
{/* Detailed Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Orgs Breakdown */}
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 space-y-3">
<div className="font-bold text-sm text-krona-primary flex items-center justify-between">
<span>Статус организаций</span>
<span className="text-xs text-krona-secondary font-normal">Всего: {totalOrgs}</span>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-status-success-text" />
<span className="text-krona-secondary">Активные организации</span>
</span>
<span className="font-bold text-krona-primary">{activeOrgs}</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-status-info-text" />
<span className="text-krona-secondary">Пробный период (Trial)</span>
</span>
<span className="font-bold text-krona-primary">{trialOrgs}</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-status-warning-text" />
<span className="text-krona-secondary">Льготный период (Grace)</span>
</span>
<span className="font-bold text-krona-primary">{graceOrgs}</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-status-danger-text" />
<span className="text-krona-secondary">Приостановлены (Suspended)</span>
</span>
<span className="font-bold text-krona-primary">{suspendedOrgs}</span>
</div>
</div>
</div>
{/* Installations Breakdown */}
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 space-y-3">
<div className="font-bold text-sm text-krona-primary flex items-center justify-between">
<span>Платформы клиентов</span>
<span className="text-xs text-krona-secondary font-normal">Всего: {installations.length}</span>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-krona-secondary">Веб-приложения (Web SPA)</span>
<span className="font-bold text-krona-primary">{webInstalls}</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-krona-secondary">Рабочие станции (Desktop)</span>
<span className="font-bold text-krona-primary">{desktopInstalls}</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-krona-secondary">Мобильные кассы (Android)</span>
<span className="font-bold text-krona-primary">{androidInstalls}</span>
</div>
{outdatedAndroid > 0 && (
<div className="flex items-center justify-between text-xs text-status-warning-text font-medium pt-1 border-t border-krona-border">
<span>Устаревшие версии Android:</span>
<span className="font-bold">{outdatedAndroid}</span>
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* ========================================================================= */}
{/* TAB 3: ИНФРАСТРУКТУРА (SERVERS & TELEMETRY) */}
{/* ========================================================================= */}
{activeTab === 'infrastructure' && (
<div className="space-y-4 animate-in fade-in duration-150" data-testid="infrastructure-section">
<div className="rounded-xl border border-krona-border bg-krona-surface p-4 space-y-3">
<div className="flex items-center justify-between">
<div>
<h3 className="font-bold text-sm text-krona-primary">Инфраструктурные серверы Krona</h3>
<p className="text-xs text-krona-secondary">Текущая загрузка вычислительных узлов платформы</p>
</div>
<button
type="button"
onClick={() => navigate('/control/infrastructure')}
className="min-h-[44px] px-3 text-xs text-krona-gold hover:underline flex items-center gap-1 font-medium"
>
<span>Управление</span>
<ArrowUpRight className="w-3.5 h-3.5" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{servers.map((srv) => (
<div
key={srv.id}
onClick={() => openInspector({ type: 'server', id: srv.id, title: srv.hostname, data: srv })}
className="p-3.5 rounded-lg border border-krona-border bg-krona-workspace cursor-pointer hover:border-krona-gold/40 transition-colors space-y-2.5"
>
<div className="flex items-center justify-between">
<div>
<div className="font-bold text-xs text-krona-primary font-mono">{srv.hostname}</div>
<div className="text-[11px] text-krona-tertiary">{srv.organizationName}</div>
</div>
<StatusBadge status={srv.status} size="sm" />
</div>
<div className="grid grid-cols-3 gap-2 text-center text-xs py-1 bg-krona-surface/40 rounded-md border border-krona-border">
<div>
<div className="text-[10px] text-krona-tertiary">CPU</div>
<div className={`font-mono font-bold ${srv.metrics.cpuPercent > 80 ? 'text-status-danger-text' : 'text-krona-primary'}`}>
{srv.metrics.cpuPercent}%
</div>
</div>
<div>
<div className="text-[10px] text-krona-tertiary">RAM</div>
<div className={`font-mono font-bold ${srv.metrics.ramPercent > 80 ? 'text-status-danger-text' : 'text-krona-primary'}`}>
{srv.metrics.ramPercent}%
</div>
</div>
<div>
<div className="text-[10px] text-krona-tertiary">Диск</div>
<div className={`font-mono font-bold ${srv.metrics.diskPercent > 85 ? 'text-status-danger-text' : 'text-krona-primary'}`}>
{srv.metrics.diskPercent}%
</div>
</div>
</div>
<div className="text-[11px] text-krona-tertiary flex items-center justify-between">
<span>Резервная копия:</span>
<span className={srv.backup.state === 'failed' ? 'text-status-danger-text font-bold' : 'text-status-success-text'}>
{srv.backup.state === 'healthy' ? 'Успешно' : srv.backup.state === 'failed' ? 'Ошибка' : srv.backup.state}
</span>
</div>
<div className="pt-1">
<HeartbeatTimeline history={srv.metrics.heartbeatHistory} compact />
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
);
};