import React, { useState } from 'react'; import { X, Smartphone, ShieldAlert, Clock, Activity, } from 'lucide-react'; import { useKccStore, useOperatorRole } from '../store/kccStore'; import { StatusBadge } from './StatusBadge'; import { MoneyDisplay } from './MoneyDisplay'; import { HeartbeatTimeline } from './HeartbeatTimeline'; import { PlatformStatus } from '../contracts'; export const InspectorDrawer: React.FC = () => { const inspectorTarget = useKccStore((state) => state.inspectorTarget); const closeInspector = useKccStore((state) => state.closeInspector); const { permissions } = useOperatorRole(); const organizations = useKccStore((state) => state.organizations); const servers = useKccStore((state) => state.servers); const incidents = useKccStore((state) => state.incidents); const modules = useKccStore((state) => state.modules); const installations = useKccStore((state) => state.installations); const subscriptions = useKccStore((state) => state.subscriptions); const instances = useKccStore((state) => state.instances); const auditLog = useKccStore((state) => state.auditLog); const openConfirmModal = useKccStore((state) => state.openConfirmModal); const updateOrganizationStatus = useKccStore((state) => state.updateOrganizationStatus); const grantModuleTrial = useKccStore((state) => state.grantModuleTrial); const resolveIncident = useKccStore((state) => state.resolveIncident); const requestServerDiagnostics = useKccStore((state) => state.requestServerDiagnostics); const scheduleServerMaintenance = useKccStore((state) => state.scheduleServerMaintenance); const [orgActiveTab, setOrgActiveTab] = useState< 'overview' | 'users' | 'modules' | 'installations' | 'billing' | 'incidents' | 'audit' >('overview'); const [selectedTrialModule, setSelectedTrialModule] = useState('krona-omnichannel'); const [trialDurationDays, setTrialDurationDays] = useState(14); if (!inspectorTarget) return null; const renderContent = () => { switch (inspectorTarget.type) { case 'organization': { const org = organizations.find((o) => o.tenantId === inspectorTarget.id); if (!org) return
Организация не найдена
; const sub = subscriptions.find((s) => s.tenantId === org.tenantId); const orgInstances = instances.filter((i) => i.tenantId === org.tenantId); const orgInstalls = installations.filter((i) => i.tenantId === org.tenantId); const orgIncidents = incidents.filter((i) => i.affectedTenantIds.includes(org.tenantId)); const orgAudits = auditLog.filter((a) => a.target.tenantId === org.tenantId || a.target.id === org.tenantId); const handleStatusChange = (newStatus: PlatformStatus) => { openConfirmModal({ title: `Смена статуса организации: ${org.displayName}`, actionName: `organization.status_changed -> ${newStatus}`, impactSummary: `Доступ всех ${org.userCount} пользователей к платформе будет переведен в состояние «${newStatus}».`, targetType: 'organization', targetId: org.tenantId, targetName: org.displayName, onConfirm: async (reason) => { await updateOrganizationStatus(org.tenantId, newStatus, reason); }, }); }; const handleGrantTrial = () => { const mod = modules.find((m) => m.moduleId === selectedTrialModule); openConfirmModal({ title: `Выдача Trial: ${mod?.name || selectedTrialModule}`, actionName: `entitlement.grant_trial (${selectedTrialModule})`, impactSummary: `Организации будет предоставлен временный доступ к модулю ${mod?.name} на ${trialDurationDays} дней без списания средств.`, targetType: 'organization', targetId: org.tenantId, targetName: org.displayName, onConfirm: async (reason) => { await grantModuleTrial(org.tenantId, selectedTrialModule, trialDurationDays, reason); }, }); }; return (
{/* Header */}
ИНН: {org.inn}

{org.displayName}

{org.domain} • ID: {org.tenantId.slice(0, 8)}...

{/* Operator Actions Bar */}
Действия оператора платформы
{org.platformStatus !== 'active' && ( )} {org.platformStatus !== 'grace' && ( )} {org.platformStatus !== 'suspended' && ( )}
{/* Trial grant inline form */} {permissions.canGrantTrialGrace && (
Выдать Trial на модуль:
)}
{/* Navigation Tabs */}
{[ { id: 'overview', label: 'Обзор' }, { id: 'users', label: `Пользователи (${org.userCount})` }, { id: 'modules', label: `Модули (${orgInstances.length})` }, { id: 'installations', label: `Установки (${orgInstalls.length})` }, { id: 'billing', label: 'Платежи' }, { id: 'incidents', label: `Инциденты (${orgIncidents.length})` }, { id: 'audit', label: 'Аудит' }, ].map((tab) => ( ))}
{/* Tab Contents */} {orgActiveTab === 'overview' && (
Тарифный план
{org.tariffPlan}
MRR доход
Филиалов / точек
{org.branchCount}
Задолженность
0 ? 'text-rose-400' : 'text-emerald-400'}`} />
Контакты организации:
Email: {org.contactEmail}
Телефон: {org.contactPhone}
Последняя активность: {new Date(org.lastActiveAt).toLocaleString('ru-RU')}
Окончание подписки: {new Date(org.subscriptionEndAt).toLocaleDateString('ru-RU')}
)} {orgActiveTab === 'users' && (
Пользователи-агрегаты {org.userCount} учетных записей

В соответствии с контрактом безопасности Control Center отображает только деперсонализированные агрегаты тенанта. Персональные данные сотрудников защищены контуром организации.

Администраторы
2
Менеджеры / операторы
{Math.max(0, org.userCount - 6)}
)} {orgActiveTab === 'modules' && (
{orgInstances.map((inst) => (
{inst.moduleName}
v{inst.currentVersion} • {inst.serverName}
))}
)} {orgActiveTab === 'installations' && (
{orgInstalls.map((ins) => (
{ins.deviceName}
{ins.kind.toUpperCase()} • v{ins.appVersion} • {ins.osVersion}
))} {orgInstalls.length === 0 &&
Нет активных установок
}
)} {orgActiveTab === 'billing' && (
{sub ? (
Тарифная подписка
Период: {sub.period === 'year' ? 'Годовой' : 'Ежемесячный'}
Сумма тарифа:
Действует до: {new Date(sub.validUntil).toLocaleDateString('ru-RU')}
) : (
Данные подписки отсутствуют
)}
)} {orgActiveTab === 'incidents' && (
{orgIncidents.map((inc) => (
{inc.incidentNumber}

{inc.summary}

))} {orgIncidents.length === 0 &&
Инцидентов нет
}
)} {orgActiveTab === 'audit' && (
{orgAudits.map((a) => (
{a.action} {new Date(a.occurredAt).toLocaleTimeString('ru-RU')}

{a.reason}

{a.operatorName} ({a.operatorRole})
))} {orgAudits.length === 0 &&
Записей аудита нет
}
)}
); } case 'server': { const srv = servers.find((s) => s.serverId === inspectorTarget.id); if (!srv) return
Сервер не найден
; const srvInstances = instances.filter((i) => i.serverId === srv.serverId); const handleDiagnostics = () => { openConfirmModal({ title: `Запрос диагностики: ${srv.name}`, actionName: 'infrastructure.request_diagnostics', impactSummary: `На узел ${srv.ipAddress} будет отправлен запрос сбора расширенного дампа метрик, журнала dmesg и состояния процессов без раскрытия секретов.`, targetType: 'infrastructure', targetId: srv.serverId, targetName: srv.name, onConfirm: async (reason) => { await requestServerDiagnostics(srv.serverId, reason); }, }); }; const handleMaintenance = () => { openConfirmModal({ title: `Запланировать ТО: ${srv.name}`, actionName: 'infrastructure.schedule_maintenance', impactSummary: `Узел ${srv.name} будет помечен как обслуживаемый. Маршрутизация трафика будет плавно перенаправлена на резервные ноды.`, targetType: 'infrastructure', targetId: srv.serverId, targetName: srv.name, onConfirm: async (reason) => { await scheduleServerMaintenance(srv.serverId, reason); }, }); }; return (
{srv.hostingProvider} • {srv.region}

{srv.name}

{srv.ipAddress} • {srv.hostname}

{/* Operator Actions */}
Операции с инфраструктурным узлом
{/* Telemetry Metrics */}
Телеметрия узла (Server Monitor Agent): Agent v{srv.agentVersion} ({srv.agentStatus})
CPU Нагрузка: {srv.cpuLoadPercent}%
80 ? 'bg-rose-500' : 'bg-emerald-500'}`} style={{ width: `${srv.cpuLoadPercent}%` }} />
RAM Память: {Math.round(srv.ramUsedBytes / (1024 * 1024 * 1024))} / {Math.round(srv.ramTotalBytes / (1024 * 1024 * 1024))} GB
Диск: {Math.round(srv.diskUsedBytes / (1024 * 1024 * 1024))} / {Math.round(srv.diskTotalBytes / (1024 * 1024 * 1024))} GB
0.85 ? 'bg-rose-500' : 'bg-amber-400'}`} style={{ width: `${(srv.diskUsedBytes / srv.diskTotalBytes) * 100}%` }} />
Сеть (Rx / Tx):
{(srv.networkRxBytesPerSec / 1024 / 1024).toFixed(1)} / {(srv.networkTxBytesPerSec / 1024 / 1024).toFixed(1)} MB/s
{/* Heartbeat timeline */}
Пульс узла (Heartbeat 24h): RTT: {srv.latencyMs}ms
{/* Backup & Restore Drill */}
Бэкап:
{srv.backup.lastSuccessfulAt ? new Date(srv.backup.lastSuccessfulAt).toLocaleString('ru-RU') : 'Нет успешных копий'}
{srv.backup.targetProvider}
Restore Drill:
{srv.restoreDrill.verifiedAt ? new Date(srv.restoreDrill.verifiedAt).toLocaleDateString('ru-RU') : 'Никогда / Устарел'}
{srv.restoreDrill.drillDurationSeconds ? `Время восстановления: ${Math.round(srv.restoreDrill.drillDurationSeconds / 60)} мин.` : 'Тест не проводился'}
{/* Module instances on server */}
Модули на узле ({srvInstances.length}):
{srvInstances.map((inst) => (
{inst.moduleName}
{inst.tenantName}
))}
); } case 'incident': { const inc = incidents.find((i) => i.incidentId === inspectorTarget.id); if (!inc) return
Инцидент не найден
; const handleResolve = () => { openConfirmModal({ title: `Разрешение инцидента ${inc.incidentNumber}`, actionName: 'incident.resolve', impactSummary: `Инцидент будет переведен в статус resolved, дежурная смена будет оповещена об устранении причин.`, targetType: 'incident', targetId: inc.incidentId, targetName: inc.incidentNumber, onConfirm: async (reason) => { await resolveIncident(inc.incidentId, reason); }, }); }; return (
{inc.severity} Severity

{inc.incidentNumber}

{inc.summary}

{inc.status !== 'resolved' && (
Действие оператора:
Отметить инцидент как устраненный с обоснованием
)}
Описание инцидента:

{inc.description}

Ответственный: {inc.ownerName}
Хронология расследования (Timeline):
{inc.timeline.map((step, idx) => (
{step.author} {new Date(step.timestamp).toLocaleTimeString('ru-RU')}

{step.message}

))}
); } default: return
Детальная информация для данного типа пока формируется
; } }; return (
{/* Drawer Header */}
Контекстный инспектор
{/* Drawer Body */}
{renderContent()}
); };