import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { AdminApiClient } from "./api/client"; import type { AuditEvent, AuditSummary, AuthResult, Cluster, ClusterAdminSummary, ClusterAuthorityState, ClusterNode, ClusterNodeGroup, CreatedJoinToken, FabricEntryPoint, FabricEntryPointNode, FabricEgressPool, FabricEgressPoolNode, FabricServiceChannelAccessTelemetry, FabricServiceChannelBreadcrumbWindowPolicy, FabricServiceChannelRecoveryPolicy, FabricServiceChannelLeaseMaintenance, FabricServiceChannelReadiness, FabricServiceChannelRouteFeedbackObservation, FabricServiceChannelRouteRebuildAlertSilence, FabricServiceChannelRouteRebuildAttempt, FabricServiceChannelRouteRebuildFeedbackHealthBreakdown, FabricServiceChannelRouteRebuildHealthSummary, FabricServiceChannelRouteRebuildIncident, FabricServiceChannelRebuildSnapshotMaintenanceHealth, FabricServiceChannelRebuildSnapshotWarmup, FabricServiceChannelSchemaStatus, FabricTestingFlag, InstallationStatus, JoinRequest, MeshLink, MeshRouteIntent, NodeHeartbeat, NodeJoinToken, NodeSyntheticMeshConfig, NodeTelemetryObservation, NodeUpdatePlan, NodeUpdateStatus, NodeWorkloadDesiredState, Organization, OrganizationAdminSummary, OrganizationMembership, QoSPolicy, ReleaseVersion, Resource, RoleAssignment, UserAccount, VPNClientDiagnosticCommand, VPNClientDiagnosticStatus, VPNConnection, VPNConnectionLease, VPNPacketStats, WorkloadStatus, } from "./types"; const storageKeys = { baseUrl: "rap.webAdmin.baseUrl", actorUserId: "rap.webAdmin.actorUserId", auth: "rap.webAdmin.auth", language: "rap.webAdmin.language", vpnDiagnosticDeviceId: "rap.webAdmin.vpnDiagnosticDeviceId", }; const defaultBaseUrl = "/api/v1"; const legacyDirectBackendPrefix = "http://192.168.200.61:8080/api/v1"; type FabricRebuildLedgerFilters = { reporterNodeId: string; routeId: string; serviceClass: string; generation: string; feedbackSource: string; feedbackChannelId: string; feedbackViolationStatus: string; offset: number; }; const defaultFabricRebuildLedgerFilters: FabricRebuildLedgerFilters = { reporterNodeId: "", routeId: "", serviceClass: "", generation: "", feedbackSource: "", feedbackChannelId: "", feedbackViolationStatus: "", offset: 0, }; const roleOptions = [ "entry-node", "relay-node", "core-mesh", "rdp-worker", "vnc-worker", "vpn-exit", "vpn-connector", "file-storage-cache", "update-cache", "video-relay", ]; const roleDisplayNames: Record = { "entry-node": "Entry node", "relay-node": "Relay node", "core-mesh": "Mesh core", "rdp-worker": "RDP worker", "vnc-worker": "VNC worker", "vpn-exit": "VPN exit", "vpn-connector": "VPN connector", "file-storage-cache": "File/cache storage", "update-cache": "Update cache", "video-relay": "Video relay", }; const capabilityKeysByRole: Record = { "entry-node": ["can_accept_client_ingress"], "relay-node": ["mesh_rendezvous_relay_control_contract", "mesh_peer_connection_manager"], "core-mesh": ["native_node_agent", "mesh_peer_connection_manager", "mesh_listener_diagnostics"], "rdp-worker": ["can_run_rdp_worker"], "vnc-worker": ["can_run_vnc_worker"], "vpn-exit": ["can_run_vpn_exit"], "vpn-connector": ["can_run_vpn_connector"], "file-storage-cache": ["can_run_file_cache"], "update-cache": ["can_run_update_cache"], "video-relay": ["can_run_video_relay"], }; const views = [ { id: "command", ru: "Обзор", en: "Command" }, { id: "clusters", ru: "Кластеры", en: "Clusters" }, { id: "cluster-settings", ru: "Настройки кластера", en: "Cluster Settings" }, { id: "nodes", ru: "Узлы", en: "Nodes" }, { id: "enrollment", ru: "Новый узел", en: "New Node" }, { id: "roles", ru: "Роли", en: "Roles" }, { id: "workloads", ru: "Сервисы", en: "Workloads" }, { id: "fabric", ru: "Связи Fabric", en: "Fabric Links" }, { id: "vpn", ru: "VPN Control", en: "VPN Control" }, { id: "servers", ru: "Серверы", en: "Servers" }, { id: "org-safe", ru: "Организации", en: "Organizations" }, { id: "audit", ru: "Аудит", en: "Audit" }, ] as const; type ViewId = (typeof views)[number]["id"]; type Language = "ru" | "en"; type ConsoleMode = "admin" | "user"; type NodeInventoryEntry = { node: ClusterNode; memberships: Array<{ cluster: Cluster; node: ClusterNode }>; }; type NodeInventoryTreeRow = | { kind: "group"; key: string; label: string; depth: number; count: number; groupId?: string } | { kind: "node"; key: string; entry: NodeInventoryEntry; depth: number }; type WebAdminSession = { userId: string; email: string; authSessionId: string; accessToken: string; refreshToken: string; accessTokenExpiresAt: string; refreshTokenExpiresAt: string; }; function normalizeStoredSession(input: unknown): WebAdminSession | null { if (!input || typeof input !== "object") { return null; } const session = input as WebAdminSession; if ( typeof session.userId !== "string" || typeof session.email !== "string" || typeof session.authSessionId !== "string" || typeof session.accessToken !== "string" || typeof session.refreshToken !== "string" || typeof session.accessTokenExpiresAt !== "string" || typeof session.refreshTokenExpiresAt !== "string" ) { return null; } if (!session.userId || !session.refreshToken) { return null; } return { userId: session.userId, email: session.email, authSessionId: session.authSessionId, accessToken: session.accessToken, refreshToken: session.refreshToken, accessTokenExpiresAt: session.accessTokenExpiresAt, refreshTokenExpiresAt: session.refreshTokenExpiresAt, }; } function isTokenExpired(expiresAt: string): boolean { const expiresAtMs = Date.parse(expiresAt); return !Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now(); } function loadStoredSession(): WebAdminSession | null { try { const stored = localStorage.getItem(storageKeys.auth); if (!stored) { return null; } const parsed = normalizeStoredSession(JSON.parse(stored)); if (!parsed || isTokenExpired(parsed.refreshTokenExpiresAt)) { return null; } return parsed; } catch { return null; } } type JoinTokenFormState = { ttlHours: number; maxUses: number; roles: string[]; nodeName: string; nodeGroupId: string; ownershipType: string; purpose: string; installMode: string; dockerImage: string; dockerContainerName: string; dockerNetwork: string; windowsStartupMode: string; windowsInstallDir: string; windowsNodeAgentSHA256: string; linuxInstallDir: string; linuxNodeAgentSHA256: string; meshListenAddr: string; meshListenPortMode: string; meshListenAutoPortStart: number; meshListenAutoPortEnd: number; meshAdvertiseEndpoint: string; meshAdvertiseTransport: string; meshConnectivityMode: string; meshNATType: string; meshRegion: string; controlPlaneEndpoint: string; artifactEndpoints: string; dockerImageArtifactSHA256: string; pullImage: boolean; replace: boolean; syntheticRuntime: boolean; }; const defaultJoinTokenForm: JoinTokenFormState = { ttlHours: 24, maxUses: 1, roles: ["core-mesh"], nodeName: "", nodeGroupId: "", ownershipType: "platform_managed", purpose: "", installMode: "docker", dockerImage: "rap-node-agent:dev-enrollment-bootstrap-smoke", dockerContainerName: "", dockerNetwork: "host", windowsStartupMode: "auto", windowsInstallDir: "", windowsNodeAgentSHA256: "", linuxInstallDir: "", linuxNodeAgentSHA256: "", meshListenAddr: ":19131", meshListenPortMode: "auto", meshListenAutoPortStart: 19131, meshListenAutoPortEnd: 19231, meshAdvertiseEndpoint: "", meshAdvertiseTransport: "direct_http", meshConnectivityMode: "private_lan", meshNATType: "none", meshRegion: "docker-test", controlPlaneEndpoint: "", artifactEndpoints: "", dockerImageArtifactSHA256: "", pullImage: false, replace: true, syntheticRuntime: true, }; const copy = { ru: { productOwner: "Владелец продукта", controlPlane: "Панель управления", sideText: "Главная панель владельца платформы для кластеров, узлов, доверия, ролей и безопасного desired state.", signInTitle: "Вход", signInText: "Введите учетные данные.", bootstrapTitle: "Первый владелец", bootstrapText: "Пустая установка принимает только подписанную активацию продукта.", activationPayload: "Activation manifest JSON", activationSignature: "Подпись manifest", createOwner: "Создать владельца", creatingOwner: "Создание...", ownerCreated: "Владелец создан. Теперь можно войти.", installationLocked: "Установка уже активирована", insecureBootstrapDisabled: "Insecure bootstrap выключен. Нужна strict-активация с ключом продукта.", email: "Логин", password: "Пароль", backendApi: "Backend API", useLocalProxy: "Использовать локальный /api/v1 proxy", language: "Язык", deviceLabel: "Устройство", rememberMe: "Запомнить меня", trustDevice: "Доверять этому устройству", signIn: "Войти", signingIn: "Вход...", logout: "Выйти", profile: "Профиль", refresh: "Обновить", refreshing: "Обновление...", autoRefresh: "Автообновление", lastRefresh: "Данные обновлены", activeCluster: "Активный кластер", slugLabel: "Технический код", slugHelp: "Slug — постоянный короткий технический идентификатор кластера для URL, скриптов, логов и интеграций. Его лучше не менять после создания.", clusterCatalog: "Каталог кластеров", clusterCatalogText: "Список реальных кластеров из Control Plane. Выберите активный кластер или раскройте карточку для подробностей.", makeActive: "Сделать активным", openSettings: "Открыть настройки", selected: "Выбран", createCluster: "Создать кластер", clusterDetails: "Подробнее", consoleTitle: "Панель владельца платформы", boundary: "WEB является только представлением. Решения кластера проходят через Control Plane API, PostgreSQL как source of truth и аудит.", noLoginError: "Войдите как владелец продукта или администратор платформы, чтобы загрузить панель.", accessDenied: "Доступ к этой панели запрещен.", sessionMode: "Режим сессии", sessionModeAdmin: "Админ", sessionModeUser: "Пользователь", sessionRefreshedAt: "Сессия обновлена", emptyLiveTitle: "Кластер пока пустой", emptyLiveText: "Это реальные данные, не заглушка: в выбранном кластере ещё нет одобренных node-agent узлов. Создайте join token, запустите rap-node-agent и подтвердите join request.", realDataNote: "Показываются только данные из PostgreSQL/Control Plane. Если значения нулевые, значит соответствующих узлов, ролей или сервисов пока нет.", signedInAs: "Вход выполнен", actorUser: "Actor user", testMode: "Тестирование", testModeText: "Включает тестовую телеметрию и синтетические наблюдения связей. Это не production mesh runtime.", platformTestFlag: "Тестирование сервера", nodeTelemetry: "Телеметрия узла", heartbeatHistory: "История heartbeat", noTelemetry: "Телеметрии пока нет", enableTelemetry: "Включить телеметрию", enableSyntheticLinks: "Включить тестовые связи", saveTestFlag: "Сохранить флаг", nodeManagement: "Управление узлом", nodeScope: "Область просмотра", currentClusterNodes: "Узлы активного кластера", allNodes: "Все узлы платформы", showAllPlatformNodes: "Показать все узлы платформы", currentClusterMembership: "Участие в активном кластере", clusterMemberships: "Участие по кластерам", notMemberOfActiveCluster: "не состоит", nodeIdentity: "Физическая идентичность узла", activeClusterScope: "Область активного кластера", activeClusterScopeText: "Один физический узел может состоять в нескольких кластерах. Роли и desired-сервисы ниже относятся только к выбранному активному кластеру.", capabilityConfirmed: "способность подтверждена heartbeat", capabilityMissing: "способность не заявлена узлом", capabilityUnknown: "способность не подтверждена: нет heartbeat", nodeGlobalInventoryText: "Один физический узел показан один раз. Участие и роли остаются кластерными: в разных кластерах этот же узел может иметь разные назначения.", nodeSearch: "Поиск узлов", groupNodesBy: "Группировать", groupByMembership: "по участию", groupByHealth: "по здоровью", groupByOwnership: "по владению", groupByClusterCount: "по числу кластеров", nodeGroups: "Группы узлов", nodeGroupTree: "Дерево групп", nodeGroupFilter: "Фильтр по группе", allNodeGroups: "Все группы", nodeGroupCreatePanel: "Создание группы", nodeGroupName: "Название группы", parentNodeGroup: "Родительская группа", rootNodeGroup: "Корень", ungroupedNodes: "Без группы", createNodeGroup: "Создать группу", createSubgroup: "Создать подгруппу", collapseGroup: "Свернуть", expandGroup: "Развернуть", assignNodeGroup: "Переместить в группу", removeFromNodeGroup: "Убрать из группы", connectExistingNode: "Подключить к активному кластеру", connectExistingNodeTitle: "Подключить существующий узел", connectExistingNodeText: "Будет создано или повторно включено участие конкретного физического узла в активном кластере. Роли ниже назначаются только в этом кластере.", connectWithRoles: "Подключить с ролями", nodeDetails: "Сведения", manageNode: "Настроить", nodeFunctions: "Функции узла", nodeFunctionsText: "Одна строка управляет функцией целиком: роль задает разрешение в активном кластере, desired-сервис задает запуск, observed показывает факт от node-agent.", rolePermission: "Разрешение", permissionGranted: "разрешено", permissionDenied: "нет разрешения", organizationScopeForEnable: "Область организации для новых включений, опционально", clusterWideRolePlaceholder: "пусто = роль на весь кластер", desiredRuntime: "Желаемое состояние", observedRuntime: "Фактически", enableFunction: "Включить функцию", disableFunction: "Выключить функцию", close: "Закрыть", nodeBriefList: "Краткий список узлов", noActiveClusterMembership: "Узел не входит в активный кластер", nodeBriefListHelp: "Список сгруппирован деревом активного кластера. Полные сведения, управление, роли, сервисы и статистика открываются из строки узла.", nodeSearchPlaceholder: "имя, ключ, кластер, статус", nodeGroupInventoryText: "Группы — это кластерная инвентарная структура. Перенос узла в группу меняет только его размещение внутри активного кластера, не роли и не членство.", nodeGroupCreated: "Группа узлов создана.", noNodesTitle: "Нет узлов", noNodesByFilter: "По текущему фильтру узлы не найдены.", cancel: "Отмена", alreadyMember: "Уже в активном кластере", revokedMembership: "Участие отозвано", addNode: "Подключить узел", addNodeText: "Подключение существующего физического узла к активному кластеру выполняется из списка узлов: включите общий режим и нажмите «Подключить к активному кластеру».", joinTokenTitle: "Создать новый Docker-узел", joinTokenText: "Сначала создается одноразовый install token и Docker install profile. Затем команда запускается на Docker-хосте, агент отправляет заявку, а владелец платформы подтверждает ее.", ttlHours: "Срок действия, часов", ttlHelp: "Через это время token станет недействительным, даже если им никто не воспользовался. Для ручного подключения обычно достаточно 1–24 часов.", maxUses: "Максимум использований", maxUsesHelp: "Сколько node-agent смогут использовать этот token. Самый безопасный вариант — 1 token на 1 новый узел.", tokenPurpose: "Назначение token", nodeOwnership: "Тип владения узлом", suggestedRoles: "Разрешенные/ожидаемые роли", generatedScope: "Сгенерированная область действия", generatedScopeHelp: "JSON формируется автоматически из настроек выше. Оператор не должен писать его руками, чтобы не ошибиться синтаксисом или областью доступа.", manualApprovalRequired: "Подтверждение заявки вручную обязательно", nodeRoles: "Роли узла", desiredServices: "Желаемые сервисы", observedServices: "Наблюдаемые сервисы", noRoles: "Ролей пока нет", noServices: "Сервисов пока нет", manageInCluster: "Управлять в кластере", rolesAndServices: "Роли и сервисы", links: "Связи", fabricMap: "Карта трафика Fabric", fabricIngressLayer: "Входы", fabricNodeLayer: "Узлы кластера", fabricEgressLayer: "Выходные зоны", observedPeerLinks: "Наблюдаемые связи", placementIntent: "control-plane назначение", fabricEntryPoints: "Точки входа", fabricEntryPointHelp: "Логические внешние входы в кластер. Они скрывают конкретные узлы от организаций и клиентов.", fabricEgressPools: "Выходные зоны", fabricEgressPoolHelp: "Логические выходы к внешним сетям, например “Офис Москва”. Организации используют зону, а не конкретный узел.", endpointName: "Название", publicEndpoint: "Публичный адрес", endpointType: "Тип входа", description: "Описание", routeScope: "Область маршрутов JSON", createEntryPoint: "Создать точку входа", createEgressPool: "Создать выходную зону", endpointNodes: "Назначенные узлы", assignEndpointNode: "Назначить узел", selectNode: "Выберите узел", assignedNodesEmpty: "Узлы пока не назначены", entryPointsEmpty: "Точки входа пока не созданы.", egressPoolsEmpty: "Выходные зоны пока не созданы.", addressNotSet: "адрес не задан", descriptionNotSet: "описание не задано", servicePlacement: "Размещение сервисов", trafficFlow: "Потоки между узлами", organizationTestFlag: "Тестирование организации", organizationId: "ID организации", saveOrganizationFlag: "Сохранить флаг организации", noLinks: "Связей пока нет", recentHeartbeats: "Последние heartbeat", memory: "Память", cpu: "Процессор", processes: "Процессы", }, en: { productOwner: "Product Owner", controlPlane: "Control Plane", sideText: "Full platform-owner panel for clusters, nodes, trust, placement, and safe service desired state.", signInTitle: "Sign in", signInText: "Enter your credentials.", bootstrapTitle: "First owner", bootstrapText: "An empty installation accepts only a signed product activation.", activationPayload: "Activation manifest JSON", activationSignature: "Manifest signature", createOwner: "Create owner", creatingOwner: "Creating...", ownerCreated: "Owner created. You can sign in now.", installationLocked: "Installation is already active", insecureBootstrapDisabled: "Insecure bootstrap is disabled. Strict product-key activation is required.", email: "Login", password: "Password", backendApi: "Backend API", useLocalProxy: "Use local /api/v1 proxy", language: "Language", deviceLabel: "Device", rememberMe: "Remember me", trustDevice: "Trust this device", signIn: "Sign in", signingIn: "Signing in...", logout: "Logout", profile: "Profile", refresh: "Refresh", refreshing: "Refreshing...", autoRefresh: "Auto refresh", lastRefresh: "Data refreshed", activeCluster: "Active cluster", slugLabel: "Technical code", slugHelp: "Slug is a stable short technical identifier for URLs, scripts, logs, and integrations. It should generally not change after creation.", clusterCatalog: "Cluster catalog", clusterCatalogText: "Real clusters from the Control Plane. Select the active cluster or expand a card for details.", makeActive: "Make active", openSettings: "Open settings", selected: "Selected", createCluster: "Create cluster", clusterDetails: "Details", consoleTitle: "Platform Owner Console", boundary: "WEB is presentation only. Cluster decisions go through Control Plane APIs, PostgreSQL source of truth, and audit.", noLoginError: "Sign in as a product owner or platform administrator to load the panel.", accessDenied: "Access to this panel is denied.", sessionMode: "Session mode", sessionModeAdmin: "Admin", sessionModeUser: "User", sessionRefreshedAt: "Session refreshed", emptyLiveTitle: "Cluster has no live nodes yet", emptyLiveText: "These are real values, not placeholders: the selected cluster has no approved node-agent nodes yet. Create a join token, run rap-node-agent, and approve the join request.", realDataNote: "Only PostgreSQL/Control Plane data is shown. Zero values mean the corresponding nodes, roles, or services do not exist yet.", signedInAs: "Signed in", actorUser: "Actor user", testMode: "Testing", testModeText: "Enables test telemetry and synthetic link observations. This is not production mesh runtime.", platformTestFlag: "Server testing", nodeTelemetry: "Node telemetry", heartbeatHistory: "Heartbeat history", noTelemetry: "No telemetry yet", enableTelemetry: "Enable telemetry", enableSyntheticLinks: "Enable test links", saveTestFlag: "Save flag", nodeManagement: "Node management", nodeScope: "View scope", currentClusterNodes: "Active cluster nodes", allNodes: "All platform nodes", showAllPlatformNodes: "Show all platform nodes", currentClusterMembership: "Active cluster membership", clusterMemberships: "Cluster memberships", notMemberOfActiveCluster: "not a member", nodeIdentity: "Physical node identity", activeClusterScope: "Active cluster scope", activeClusterScopeText: "One physical node may belong to multiple clusters. Roles and desired services below belong only to the selected active cluster.", capabilityConfirmed: "capability confirmed by heartbeat", capabilityMissing: "capability not reported by node", capabilityUnknown: "capability unconfirmed: no heartbeat", nodeGlobalInventoryText: "Each physical node is shown once. Membership and roles remain cluster-scoped, so the same node may have different assignments in different clusters.", nodeSearch: "Node search", groupNodesBy: "Group by", groupByMembership: "membership", groupByHealth: "health", groupByOwnership: "ownership", groupByClusterCount: "cluster count", nodeGroups: "Node groups", nodeGroupTree: "Group tree", nodeGroupFilter: "Group filter", allNodeGroups: "All groups", nodeGroupCreatePanel: "Create group", nodeGroupName: "Group name", parentNodeGroup: "Parent group", rootNodeGroup: "Root", ungroupedNodes: "Ungrouped", createNodeGroup: "Create group", createSubgroup: "Create subgroup", collapseGroup: "Collapse", expandGroup: "Expand", assignNodeGroup: "Move to group", removeFromNodeGroup: "Remove from group", connectExistingNode: "Connect to active cluster", connectExistingNodeTitle: "Connect existing node", connectExistingNodeText: "This creates or re-enables membership for one concrete physical node in the active cluster. Roles below are assigned only in this cluster.", connectWithRoles: "Connect with roles", nodeDetails: "Details", manageNode: "Configure", nodeFunctions: "Node functions", nodeFunctionsText: "One row controls the whole function: role grants permission in the active cluster, desired service requests runtime start, observed state reports node-agent facts.", rolePermission: "Permission", permissionGranted: "granted", permissionDenied: "not allowed", organizationScopeForEnable: "Organization scope for new enables, optional", clusterWideRolePlaceholder: "empty = cluster-wide role", desiredRuntime: "Desired state", observedRuntime: "Observed", enableFunction: "Enable function", disableFunction: "Disable function", close: "Close", nodeBriefList: "Compact node list", noActiveClusterMembership: "Node is not a member of the active cluster", nodeBriefListHelp: "The list is grouped as the active cluster tree. Full details, management, roles, services, and statistics open from the node row.", nodeSearchPlaceholder: "name, key, cluster, status", nodeGroupInventoryText: "Groups are a cluster inventory structure. Moving a node to a group changes only its placement inside the active cluster, not roles or membership.", nodeGroupCreated: "Node group created.", noNodesTitle: "No nodes", noNodesByFilter: "No nodes match the current filter.", cancel: "Cancel", alreadyMember: "Already in active cluster", revokedMembership: "Membership revoked", addNode: "Add node", addNodeText: "Connect an existing physical node to the active cluster from the node list: enable platform-wide view and click “Connect to active cluster”.", joinTokenTitle: "Create new Docker node", joinTokenText: "First create a one-time install token and Docker install profile. Then run the generated command on the Docker host; the agent submits a request and the platform owner approves it.", ttlHours: "Lifetime, hours", ttlHelp: "After this time the token becomes invalid even if unused. For manual enrollment, 1–24 hours is usually enough.", maxUses: "Maximum uses", maxUsesHelp: "How many node-agents may use this token. The safest default is one token for one new node.", tokenPurpose: "Token purpose", nodeOwnership: "Node ownership type", suggestedRoles: "Allowed/expected roles", generatedScope: "Generated scope", generatedScopeHelp: "JSON is generated automatically from the settings above. Operators should not hand-write it and risk syntax or access-scope mistakes.", manualApprovalRequired: "Manual request approval is required", nodeRoles: "Node roles", desiredServices: "Desired services", observedServices: "Observed services", noRoles: "No roles yet", noServices: "No services yet", manageInCluster: "Manage in cluster", rolesAndServices: "Roles and services", links: "Links", fabricMap: "Fabric traffic map", fabricIngressLayer: "Ingress", fabricNodeLayer: "Cluster nodes", fabricEgressLayer: "Egress pools", observedPeerLinks: "Observed links", placementIntent: "control-plane placement", fabricEntryPoints: "Entry points", fabricEntryPointHelp: "Logical external ingress points for the cluster. They hide concrete nodes from organizations and clients.", fabricEgressPools: "Egress pools", fabricEgressPoolHelp: "Logical exits to external networks, for example “Office Moscow”. Organizations use the pool, not a concrete node.", endpointName: "Name", publicEndpoint: "Public endpoint", endpointType: "Entry type", description: "Description", routeScope: "Route scope JSON", createEntryPoint: "Create entry point", createEgressPool: "Create egress pool", endpointNodes: "Assigned nodes", assignEndpointNode: "Assign node", selectNode: "Select node", assignedNodesEmpty: "No nodes assigned yet", entryPointsEmpty: "No entry points created yet.", egressPoolsEmpty: "No egress pools created yet.", addressNotSet: "address not set", descriptionNotSet: "description not set", servicePlacement: "Service placement", trafficFlow: "Node traffic flows", organizationTestFlag: "Organization testing", organizationId: "Organization ID", saveOrganizationFlag: "Save organization flag", noLinks: "No links yet", recentHeartbeats: "Recent heartbeats", memory: "Memory", cpu: "CPU", processes: "Processes", }, } satisfies Record>; function normalizeAuthResult(result: AuthResult): WebAdminSession { const userId = result.user.id || result.user.ID || ""; const email = result.user.email || result.user.Email || ""; const authSessionId = result.auth_session.id || result.auth_session.ID || ""; return { userId, email, authSessionId, accessToken: result.tokens.access_token, refreshToken: result.tokens.refresh_token, accessTokenExpiresAt: result.tokens.access_token_expires_at, refreshTokenExpiresAt: result.tokens.refresh_token_expires_at, }; } async function resolveConsoleMode(clientByUser: AdminApiClient): Promise { try { await clientByUser.listClusterSummaries(); return "admin"; } catch { try { await Promise.all([clientByUser.listOrganizations(), clientByUser.listResources()]); return "user"; } catch { return null; } } } export function App() { const [authRestorationAttempted, setAuthRestorationAttempted] = useState(false); const [rememberSession, setRememberSession] = useState(() => !!loadStoredSession()); const [baseUrl] = useState(() => { const stored = localStorage.getItem(storageKeys.baseUrl)?.trim(); return !stored || stored.startsWith(legacyDirectBackendPrefix) ? defaultBaseUrl : stored; }); const [session, setSession] = useState(() => loadStoredSession()); const [consoleMode, setConsoleMode] = useState(null); const [sessionRefreshedAt, setSessionRefreshedAt] = useState(""); const [language, setLanguage] = useState(() => (localStorage.getItem(storageKeys.language) === "en" ? "en" : "ru")); const [actorUserId, setActorUserId] = useState(session?.userId ?? localStorage.getItem(storageKeys.actorUserId) ?? ""); const [loginForm, setLoginForm] = useState({ email: "", password: "", deviceLabel: "Панель владельца платформы", trustDevice: true, rememberMe: true, showPassword: false, }); const [installationStatus, setInstallationStatus] = useState(null); const [bootstrapForm, setBootstrapForm] = useState({ email: "", password: "", activationPayload: "", activationSignature: "", }); const [activeView, setActiveView] = useState("command"); const [selectedClusterId, setSelectedClusterId] = useState(""); const [clusters, setClusters] = useState([]); const [clusterSummaries, setClusterSummaries] = useState([]); const [authority, setAuthority] = useState(null); const [nodes, setNodes] = useState([]); const [nodeGroups, setNodeGroups] = useState([]); const [allNodesByCluster, setAllNodesByCluster] = useState>({}); const [joinRequests, setJoinRequests] = useState([]); const [joinTokens, setJoinTokens] = useState([]); const [releaseVersions, setReleaseVersions] = useState([]); const [nodeUpdatePlansByNode, setNodeUpdatePlansByNode] = useState>({}); const [nodeUpdateStatusesByNode, setNodeUpdateStatusesByNode] = useState>({}); const [rolesByNode, setRolesByNode] = useState>({}); const [desiredWorkloadsByNode, setDesiredWorkloadsByNode] = useState>({}); const [workloadsByNode, setWorkloadsByNode] = useState>({}); const [heartbeatsByNode, setHeartbeatsByNode] = useState>({}); const [telemetryByNode, setTelemetryByNode] = useState>({}); const [meshLinks, setMeshLinks] = useState([]); const [routeIntents, setRouteIntents] = useState([]); const [syntheticMeshConfigsByNode, setSyntheticMeshConfigsByNode] = useState>({}); const [fabricRouteFeedback, setFabricRouteFeedback] = useState([]); const [fabricRebuildAttempts, setFabricRebuildAttempts] = useState([]); const [fabricRebuildHealth, setFabricRebuildHealth] = useState(null); const [fabricRebuildSilences, setFabricRebuildSilences] = useState([]); const [fabricReadiness, setFabricReadiness] = useState(null); const [fabricSchemaStatus, setFabricSchemaStatus] = useState(null); const [fabricSnapshotHealth, setFabricSnapshotHealth] = useState(null); const [fabricSnapshotWarmup, setFabricSnapshotWarmup] = useState(null); const [fabricLeaseMaintenance, setFabricLeaseMaintenance] = useState(null); const [fabricAccessTelemetry, setFabricAccessTelemetry] = useState(null); const [fabricRebuildIncidents, setFabricRebuildIncidents] = useState([]); const [fabricRebuildLedgerDeep, setFabricRebuildLedgerDeep] = useState(false); const [fabricRebuildLedgerFilters, setFabricRebuildLedgerFilters] = useState(defaultFabricRebuildLedgerFilters); const [fabricRecoveryPolicy, setFabricRecoveryPolicy] = useState(null); const [fabricBreadcrumbWindowPolicy, setFabricBreadcrumbWindowPolicy] = useState(null); const [qosPolicies, setQosPolicies] = useState([]); const [entryPoints, setEntryPoints] = useState([]); const [entryPointNodesById, setEntryPointNodesById] = useState>({}); const [egressPools, setEgressPools] = useState([]); const [egressPoolNodesById, setEgressPoolNodesById] = useState>({}); const [testingFlags, setTestingFlags] = useState([]); const [vpnConnections, setVPNConnections] = useState([]); const [vpnLeases, setVPNLeases] = useState>({}); const [vpnPacketStats, setVPNPacketStats] = useState>({}); const [vpnDiagnosticDeviceId, setVPNDiagnosticDeviceId] = useState(() => localStorage.getItem(storageKeys.vpnDiagnosticDeviceId) || ""); const [vpnClientDiagnostics, setVPNClientDiagnostics] = useState([]); const [vpnClientDiagnostic, setVPNClientDiagnostic] = useState(null); const [vpnDiagnosticTestUrl, setVPNDiagnosticTestUrl] = useState("http://2ip.ru/"); const [lastVPNDiagnosticCommand, setLastVPNDiagnosticCommand] = useState(null); const [organizations, setOrganizations] = useState([]); const [users, setUsers] = useState([]); const [resources, setResources] = useState([]); const [membershipsByOrg, setMembershipsByOrg] = useState>({}); const [audit, setAudit] = useState([]); const [fabricDrilldownAudit, setFabricDrilldownAudit] = useState([]); const [fabricDrilldownAuditSummary, setFabricDrilldownAuditSummary] = useState(null); const [lastDataRefreshAt, setLastDataRefreshAt] = useState(""); const [liveTransport, setLiveTransport] = useState<"sse" | "poll">("poll"); const [organizationId, setOrganizationId] = useState(""); const [organizationSummary, setOrganizationSummary] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [notice, setNotice] = useState(""); const [clusterForm, setClusterForm] = useState({ slug: "", name: "", region: "" }); const [clusterSettingsForm, setClusterSettingsForm] = useState({ name: "", status: "active", region: "", metadataJson: "{}" }); const [nodeGroupForm, setNodeGroupForm] = useState({ name: "", parentGroupId: "" }); const [entryPointForm, setEntryPointForm] = useState({ name: "", endpointType: "client_access", publicEndpoint: "" }); const [egressPoolForm, setEgressPoolForm] = useState({ name: "", description: "", routeScope: "{\n \"routes\": []\n}" }); const [fabricRecoveryPolicyForm, setFabricRecoveryPolicyForm] = useState({ hysteresisPenalty: "150", promotionMinSamples: "64", demotionFailureThreshold: "1", demotionDropThreshold: "1", demotionSlowThreshold: "1", demotionRebuildEnabled: true, demotionFencedEnabled: true, }); const [fabricBreadcrumbWindowPolicyForm, setFabricBreadcrumbWindowPolicyForm] = useState({ currentWindowSeconds: "1800", historyWindowSeconds: "86400", }); const [joinTokenForm, setJoinTokenForm] = useState(defaultJoinTokenForm); const [lastJoinToken, setLastJoinToken] = useState(null); const [authorityForm, setAuthorityForm] = useState({ authorityState: "authoritative", mutationMode: "normal", notes: "" }); const [roleOrgScope, setRoleOrgScope] = useState(""); const [nodeViewScope, setNodeViewScope] = useState<"cluster" | "all">("cluster"); const [allNodeSearch, setAllNodeSearch] = useState(""); const [nodeGroupFilterId, setNodeGroupFilterId] = useState(""); const [collapsedNodeGroupKeys, setCollapsedNodeGroupKeys] = useState([]); const [allNodeGroupBy, setAllNodeGroupBy] = useState<"membership" | "health" | "ownership" | "cluster_count">("membership"); const [attachNodeDialog, setAttachNodeDialog] = useState<{ node: ClusterNode; memberships: Array<{ cluster: Cluster; node: ClusterNode }> } | null>(null); const [attachNodeRoles, setAttachNodeRoles] = useState([]); const [nodeInfoDialog, setNodeInfoDialog] = useState(null); const [nodeInfoMode, setNodeInfoMode] = useState<"details" | "manage">("details"); const [nodeRoleDrafts, setNodeRoleDrafts] = useState>({}); const [nodeWorkloadDrafts, setNodeWorkloadDrafts] = useState>({}); const [meshListenerDrafts, setMeshListenerDrafts] = useState< Record >({}); const [entryPointNodeDrafts, setEntryPointNodeDrafts] = useState>({}); const [egressPoolNodeDrafts, setEgressPoolNodeDrafts] = useState>({}); const [nodeTestingDrafts, setNodeTestingDrafts] = useState>({}); const [testingOrgId, setTestingOrgId] = useState(""); const [testingOrgDraft, setTestingOrgDraft] = useState({ telemetry: true, links: true }); const [workloadForm, setWorkloadForm] = useState({ nodeId: "", serviceType: "entry-node", desiredState: "enabled", runtimeMode: "container", version: "", configJson: "{}", environmentJson: "{}", }); const [vpnForm, setVPNForm] = useState({ organizationId: "", name: "", protocolFamily: "generic", desiredState: "disabled", credentialRef: "", targetEndpointJson: "{}", allowedNodePolicyJson: `{"mode":"explicit","node_ids":[]}`, routingUsageJson: "[]", routePolicyJson: "{}", qosPolicyJson: "{}", placementPolicyJson: "{}", }); const [organizationForm, setOrganizationForm] = useState({ slug: "", name: "" }); const [userForm, setUserForm] = useState({ email: "", password: "", platformRole: "user" }); const [membershipForm, setMembershipForm] = useState({ organizationId: "", userId: "", roleId: "org_member" }); const [resourceSecretDialog, setResourceSecretDialog] = useState(null); const [resourceSecretForm, setResourceSecretForm] = useState({ username: "", password: "", domain: "" }); const [resourceForm, setResourceForm] = useState({ organizationId: "", name: "", address: "", protocol: "rdp", routeMode: "vpn_exit", entryNode: "", exitNode: "", tags: "", username: "", password: "", domain: "", }); const [androidClientVersion, setAndroidClientVersion] = useState(""); const [androidClientPublishedAt, setAndroidClientPublishedAt] = useState(""); const [androidClientVersionedPath, setAndroidClientVersionedPath] = useState(""); const androidClientDefaultLatestFilename = "rap-android-rdp-vpn-latest-release.apk"; const [androidClientLatestPath, setAndroidClientLatestPath] = useState(androidClientDefaultLatestFilename); const client = useMemo(() => new AdminApiClient({ baseUrl, actorUserId }), [baseUrl, actorUserId]); const authClient = useMemo(() => new AdminApiClient({ baseUrl, actorUserId: "" }), [baseUrl]); const clusterScopeRequestSeq = useRef(0); const autoRefreshInFlight = useRef(false); const t = copy[language]; const selectedCluster = clusters.find((cluster) => cluster.id === selectedClusterId) || null; const selectedSummary = clusterSummaries.find((summary) => summary.cluster_id === selectedClusterId) || null; const portalDownloadBaseUrl = useMemo(() => downloadBaseUrl(baseUrl), [baseUrl]); const resolvePortalDownloadPath = useCallback((artifactPath: string | undefined, fallbackName: string) => { if (!artifactPath) { return fallbackName; } const normalized = artifactPath.trim(); if (!normalized) { return fallbackName; } if (/^https?:\/\//i.test(normalized) || normalized.startsWith("/")) { if (normalized.startsWith("/")) { return normalized.substring(1); } return normalized; } if (normalized.startsWith("downloads/")) { return normalized; } return `downloads/${normalized.replace(/^\.\/+/, "").replace(/^\/+/, "")}`; }, []); const portalAndroidLatestName = resolvePortalDownloadPath(androidClientLatestPath, androidClientDefaultLatestFilename); const portalAndroidVersionedName = androidClientVersionedPath ? resolvePortalDownloadPath(androidClientVersionedPath, portalAndroidLatestName) : portalAndroidLatestName; const portalAndroidArtifactPath = androidClientVersionedPath ? portalAndroidVersionedName : portalAndroidLatestName; const portalAndroidDownloadHref = /^https?:\/\//i.test(portalAndroidArtifactPath) ? portalAndroidArtifactPath : `${portalDownloadBaseUrl}/${portalAndroidArtifactPath}`; const androidVPNDownloadUrl = `${portalAndroidDownloadHref}${ androidClientPublishedAt ? `?_v=${encodeURIComponent(androidClientPublishedAt)}` : "" }`; const joinTokenScope = useMemo(() => buildJoinTokenScope(joinTokenForm), [joinTokenForm]); const lastJoinTokenInstallForm = useMemo( () => (lastJoinToken ? joinTokenFormFromScope(lastJoinToken.scope, joinTokenForm) : joinTokenForm), [lastJoinToken, joinTokenForm], ); const allNodeInventory = useMemo(() => { const grouped = new Map< string, { node: ClusterNode; memberships: Array<{ cluster: Cluster; node: ClusterNode }>; } >(); for (const cluster of clusters) { for (const node of allNodesByCluster[cluster.id] || []) { const current = grouped.get(node.id); if (current) { current.memberships.push({ cluster, node }); if ((node.last_seen_at || "") > (current.node.last_seen_at || "")) { current.node = node; } } else { grouped.set(node.id, { node, memberships: [{ cluster, node }] }); } } } return Array.from(grouped.values()).sort((left, right) => left.node.name.localeCompare(right.node.name)); }, [allNodesByCluster, clusters]); const groupedAllNodeInventory = useMemo( () => groupNodeInventory(allNodeInventory, selectedClusterId, allNodeSearch, allNodeGroupBy, language), [allNodeInventory, allNodeGroupBy, allNodeSearch, language, selectedClusterId], ); const visibleNodeInventory = useMemo(() => { const normalizedSearch = allNodeSearch.trim().toLowerCase(); const allowedGroupIds = nodeGroupFilterId ? new Set([nodeGroupFilterId, ...descendantGroupIds(nodeGroupFilterId, nodeGroups)]) : null; return allNodeInventory.filter((entry) => { const hasActiveClusterMembership = entry.memberships.some((membership) => membership.cluster.id === selectedClusterId); if (nodeViewScope !== "all" && !hasActiveClusterMembership) { return false; } if (allowedGroupIds) { const activeMembership = entry.memberships.find((membership) => membership.cluster.id === selectedClusterId); if (!activeMembership?.node.node_group_id || !allowedGroupIds.has(activeMembership.node.node_group_id)) { return false; } } return !normalizedSearch || nodeInventoryMatches(entry, normalizedSearch); }); }, [allNodeInventory, allNodeSearch, nodeGroupFilterId, nodeGroups, nodeViewScope, selectedClusterId]); const persistSession = useCallback((nextSession: WebAdminSession | null, remember = false) => { if (nextSession && remember) { localStorage.setItem(storageKeys.auth, JSON.stringify(nextSession)); localStorage.setItem(storageKeys.actorUserId, nextSession.userId); setRememberSession(true); return; } setRememberSession(false); localStorage.removeItem(storageKeys.auth); localStorage.removeItem(storageKeys.actorUserId); }, []); const updateAndroidPortalMetadata = useCallback(async () => { try { const manifestUrl = `${portalDownloadBaseUrl}/downloads/rap-android-rdp-vpn-build.json?_cb=${Date.now()}`; const response = await fetch(manifestUrl, { cache: "no-store" }); if (!response.ok) { setAndroidClientVersion(""); setAndroidClientPublishedAt(new Date().toISOString()); setAndroidClientVersionedPath(""); setAndroidClientLatestPath(androidClientDefaultLatestFilename); return; } const payload = (await response.json()) as { version?: { name?: string }; published?: { timestamp_utc?: string; path?: string }; release_paths?: { latest?: string; versioned?: string; }; }; setAndroidClientVersion(payload.version?.name || ""); setAndroidClientPublishedAt(payload.published?.timestamp_utc || ""); setAndroidClientVersionedPath(payload.release_paths?.versioned || ""); setAndroidClientLatestPath(payload.published?.path || payload.release_paths?.latest || androidClientDefaultLatestFilename); } catch { setAndroidClientVersion(""); setAndroidClientPublishedAt(new Date().toISOString()); setAndroidClientVersionedPath(""); setAndroidClientLatestPath(androidClientDefaultLatestFilename); } }, [portalDownloadBaseUrl]); const visibleNodeTreeRows = useMemo( () => buildNodeInventoryTreeRows(visibleNodeInventory, nodeGroups, selectedClusterId, t, new Set(collapsedNodeGroupKeys)), [collapsedNodeGroupKeys, nodeGroups, selectedClusterId, t, visibleNodeInventory], ); const fabricDrilldownAuditEvents = useMemo( () => fabricDrilldownAudit.slice(0, 8), [fabricDrilldownAudit], ); useEffect(() => { if (authRestorationAttempted) { return; } setAuthRestorationAttempted(true); const storedSession = loadStoredSession(); if (!storedSession) { return; } if (isTokenExpired(storedSession.refreshTokenExpiresAt)) { localStorage.removeItem(storageKeys.auth); localStorage.removeItem(storageKeys.actorUserId); setRememberSession(false); return; } const attemptRestore = async () => { try { const result = await authClient.refresh({ refreshToken: storedSession.refreshToken, }); const restored = normalizeAuthResult(result); if (!restored.userId || !restored.authSessionId) { throw new Error("Не удалось восстановить сессию."); } const accessClient = new AdminApiClient({ baseUrl, actorUserId: restored.userId }); const mode = await resolveConsoleMode(accessClient); if (!mode) { throw new Error("Доступ к этой панели запрещен."); } setActorUserId(restored.userId); persistSession(restored, true); setSession(restored); setSessionRefreshedAt(new Date().toISOString()); setLoginForm((previous) => ({ ...previous, email: restored.email })); setConsoleMode(mode); } catch { localStorage.removeItem(storageKeys.auth); localStorage.removeItem(storageKeys.actorUserId); setRememberSession(false); setSessionRefreshedAt(""); setSession(null); setActorUserId(""); setConsoleMode(null); } }; void attemptRestore(); }, [authClient, authRestorationAttempted, baseUrl, persistSession]); useEffect(() => { let cancelled = false; authClient .getInstallationStatus() .then((status) => { if (!cancelled) { setInstallationStatus(status); } }) .catch((err) => { if (!cancelled) { setError(err instanceof Error ? err.message : "Не удалось загрузить статус установки."); } }); return () => { cancelled = true; }; }, [authClient]); useEffect(() => { if (!selectedCluster) { setClusterSettingsForm({ name: "", status: "active", region: "", metadataJson: "{}" }); return; } setClusterSettingsForm({ name: selectedCluster.name, status: selectedCluster.status || "active", region: selectedCluster.region || "", metadataJson: JSON.stringify(selectedCluster.metadata || {}, null, 2), }); }, [selectedCluster]); useEffect(() => { setNodeGroupFilterId(""); setNodeGroupForm({ name: "", parentGroupId: "" }); setCollapsedNodeGroupKeys([]); }, [selectedClusterId]); useEffect(() => { setAttachNodeDialog(null); setAttachNodeRoles([]); }, [selectedClusterId]); useEffect(() => { localStorage.setItem(storageKeys.baseUrl, baseUrl); localStorage.setItem(storageKeys.language, language); if (session) { localStorage.setItem(`${storageKeys.language}.${session.userId}`, language); } if (!session || !rememberSession) { localStorage.removeItem(storageKeys.auth); localStorage.removeItem(storageKeys.actorUserId); } }, [baseUrl, language, rememberSession, session]); useEffect(() => { if (!session) { return; } const userLanguage = localStorage.getItem(`${storageKeys.language}.${session.userId}`); if (userLanguage === "ru" || userLanguage === "en") { setLanguage(userLanguage); } }, [session?.userId]); useEffect(() => { if (session) { void refreshAll(); } // First load only. The operator keeps refresh timing explicit after that. // eslint-disable-next-line react-hooks/exhaustive-deps }, [session?.userId]); useEffect(() => { if (!session || consoleMode !== "admin" || !selectedClusterId) { return; } let cancelled = false; const tick = () => { if (cancelled || loading || autoRefreshInFlight.current || document.visibilityState === "hidden") { return; } autoRefreshInFlight.current = true; refreshLiveData(selectedClusterId) .catch((err) => { if (!cancelled) { setError(err instanceof Error ? err.message : "Не удалось автообновить данные панели."); } }) .finally(() => { autoRefreshInFlight.current = false; }); }; let eventSource: EventSource | null = null; if (typeof window.EventSource === "function") { eventSource = new EventSource(client.clusterEventsURL(selectedClusterId)); eventSource.onopen = () => { if (!cancelled) { setLiveTransport("sse"); } }; eventSource.onerror = () => { if (!cancelled) { setLiveTransport("poll"); } }; eventSource.addEventListener("cluster.changed", tick); } const interval = window.setInterval(tick, eventSource ? 30_000 : 10_000); return () => { cancelled = true; eventSource?.close(); window.clearInterval(interval); }; }, [client, consoleMode, loading, selectedClusterId, session?.userId]); async function refreshAll(preferredClusterId = selectedClusterId) { if (!actorUserId.trim()) { setError(t.noLoginError); return; } if (consoleMode === "user") { await refreshUserPortal(); return; } setLoading(true); setError(""); setNotice(""); try { const [loadedClusters, loadedSummaries, loadedOrganizations, loadedUsers, loadedResources] = await Promise.all([ client.listClusters(), client.listClusterSummaries(), client.listOrganizations(), client.listUsers(), client.listResources(), ]); setClusters(loadedClusters); setClusterSummaries(loadedSummaries); setOrganizations(loadedOrganizations); setUsers(loadedUsers); setResources(loadedResources); if (!organizationId && loadedOrganizations[0]?.id) { setOrganizationId(loadedOrganizations[0].id); } setMembershipForm((previous) => ({ ...previous, organizationId: previous.organizationId || loadedOrganizations[0]?.id || "" })); setResourceForm((previous) => ({ ...previous, organizationId: previous.organizationId || loadedOrganizations[0]?.id || "" })); const membershipEntries = await Promise.all( loadedOrganizations.map(async (org) => [org.id, await client.listOrganizationMemberships(org.id)] as const), ); setMembershipsByOrg(Object.fromEntries(membershipEntries)); const allNodeEntries = await Promise.all(loadedClusters.map(async (cluster) => [cluster.id, await client.listNodes(cluster.id)] as const)); setAllNodesByCluster(Object.fromEntries(allNodeEntries)); const clusterId = preferredClusterId || loadedClusters[0]?.id || ""; setSelectedClusterId(clusterId); if (clusterId) { await loadClusterScope(clusterId); } setLastDataRefreshAt(new Date().toISOString()); } catch (err) { setError(err instanceof Error ? err.message : "Неизвестная ошибка панели управления платформой."); } finally { setLoading(false); } } async function refreshUserPortal() { if (!actorUserId.trim()) { setError("Войдите, чтобы загрузить личный кабинет."); return; } setLoading(true); setError(""); setNotice(""); try { await updateAndroidPortalMetadata(); const [loadedOrganizations, loadedResources] = await Promise.all([client.listOrganizations(), client.listResources()]); setOrganizations(loadedOrganizations); setResources(loadedResources); if (!organizationId && loadedOrganizations[0]?.id) { setOrganizationId(loadedOrganizations[0].id); } const membershipEntries = await Promise.all( loadedOrganizations.map(async (org) => [org.id, await client.listOrganizationMemberships(org.id)] as const), ); setMembershipsByOrg(Object.fromEntries(membershipEntries)); setLastDataRefreshAt(new Date().toISOString()); } catch (err) { setError(err instanceof Error ? err.message : "Не удалось загрузить личный кабинет."); } finally { setLoading(false); } } async function refreshLiveData(clusterId: string) { if (!actorUserId.trim()) { return; } const [loadedSummaries, loadedNodes, loadedOrganizations, loadedUsers, loadedResources] = await Promise.all([ client.listClusterSummaries(), client.listNodes(clusterId), client.listOrganizations(), client.listUsers(), client.listResources(), ]); setClusterSummaries(loadedSummaries); setOrganizations(loadedOrganizations); setUsers(loadedUsers); setResources(loadedResources); setAllNodesByCluster((previous) => ({ ...previous, [clusterId]: loadedNodes })); await loadClusterScope(clusterId, { preserveEditableForms: true }); setLastDataRefreshAt(new Date().toISOString()); } async function loadClusterScope(clusterId: string, options: { preserveEditableForms?: boolean } = {}) { const requestSeq = ++clusterScopeRequestSeq.current; const rebuildLedgerLimit = fabricRebuildLedgerDeep ? 20 : 10; const rebuildLedgerOffset = fabricRebuildLedgerDeep ? fabricRebuildLedgerFilters.offset : 0; const rebuildLedgerInput = { reporterNodeId: fabricRebuildLedgerFilters.reporterNodeId || undefined, routeId: fabricRebuildLedgerFilters.routeId || undefined, serviceClass: fabricRebuildLedgerFilters.serviceClass || undefined, generation: fabricRebuildLedgerFilters.generation || undefined, feedbackSource: fabricRebuildLedgerFilters.feedbackSource || undefined, feedbackChannelId: fabricRebuildLedgerFilters.feedbackChannelId || undefined, feedbackViolationStatus: fabricRebuildLedgerFilters.feedbackViolationStatus || undefined, limit: rebuildLedgerLimit, offset: rebuildLedgerOffset, enrichment: fabricRebuildLedgerDeep ? ("deep" as const) : ("summary" as const), }; const [ loadedNodes, loadedNodeGroups, loadedJoinRequests, loadedJoinTokens, loadedReleaseVersions, loadedAuthority, loadedAudit, loadedFabricDrilldownAuditResult, loadedMeshLinks, loadedRouteIntents, loadedFabricRouteFeedback, loadedFabricRebuildAttempts, loadedFabricRebuildHealth, loadedFabricRebuildSilences, loadedFabricReadiness, loadedFabricSchemaStatus, loadedFabricSnapshotHealth, loadedFabricLeaseMaintenance, loadedFabricAccessTelemetry, loadedFabricRebuildIncidents, loadedFabricRecoveryPolicy, loadedFabricBreadcrumbWindowPolicy, loadedQosPolicies, loadedEntryPoints, loadedEgressPools, loadedVPNConnections, loadedTestingFlags, ] = await Promise.all([ client.listNodes(clusterId), client.listNodeGroups(clusterId), client.listJoinRequests(clusterId), client.listJoinTokens(clusterId), client.listReleaseVersions(clusterId, "rap-node-agent", "dev"), client.getClusterAuthority(clusterId), client.listAudit(clusterId), client.getFabricServiceChannelRebuildInvestigationBreadcrumbs(clusterId, { limit: 20 }), client.listMeshLinks(clusterId), client.listRouteIntents(clusterId), client.listFabricServiceChannelRouteFeedback(clusterId, { includeExpired: true }), client.listFabricServiceChannelRouteRebuildAttempts(clusterId, rebuildLedgerInput), client.getFabricServiceChannelRouteRebuildHealthSummary(clusterId, { limit: 5 }), client.listFabricServiceChannelRouteRebuildAlertSilences(clusterId), client.getFabricServiceChannelReadiness(clusterId, { limit: 5 }), client.getFabricServiceChannelSchemaStatus(clusterId), client.getFabricServiceChannelRebuildSnapshotMaintenanceHealth(clusterId, { limit: 50, minAgeSeconds: 60, heartbeatThreshold: 2 }), client.getFabricServiceChannelLeaseMaintenance(clusterId, { limit: 20, includeExpired: true }), client.getFabricServiceChannelAccessTelemetry(clusterId, { limit: 20 }), client.listFabricServiceChannelRouteRebuildIncidents(clusterId, { limit: 5 }), client.getFabricServiceChannelRecoveryPolicy(clusterId), client.getFabricServiceChannelBreadcrumbWindowPolicy(clusterId), client.listQoSPolicies(clusterId), client.listFabricEntryPoints(clusterId), client.listFabricEgressPools(clusterId), client.listVPNConnections(clusterId), client.listFabricTestingFlags(), ]); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setNodes(loadedNodes); setNodeGroups(loadedNodeGroups); setJoinRequests(loadedJoinRequests); setJoinTokens(loadedJoinTokens); setReleaseVersions(loadedReleaseVersions); setAuthority(loadedAuthority); if (!options.preserveEditableForms) { setAuthorityForm({ authorityState: loadedAuthority.authority_state, mutationMode: loadedAuthority.mutation_mode, notes: loadedAuthority.notes || "", }); } setAudit(loadedAudit); setFabricDrilldownAudit(loadedFabricDrilldownAuditResult.events); setFabricDrilldownAuditSummary(loadedFabricDrilldownAuditResult.summary || null); setMeshLinks(loadedMeshLinks); setRouteIntents(loadedRouteIntents); setFabricRouteFeedback(loadedFabricRouteFeedback); setFabricRebuildAttempts(loadedFabricRebuildAttempts); setFabricRebuildHealth(loadedFabricRebuildHealth); setFabricRebuildSilences(loadedFabricRebuildSilences); setFabricReadiness(loadedFabricReadiness); setFabricSchemaStatus(loadedFabricSchemaStatus); setFabricSnapshotHealth(loadedFabricSnapshotHealth); setFabricLeaseMaintenance(loadedFabricLeaseMaintenance); setFabricAccessTelemetry(loadedFabricAccessTelemetry); setFabricRebuildIncidents(loadedFabricRebuildIncidents); setFabricRecoveryPolicy(loadedFabricRecoveryPolicy); setFabricBreadcrumbWindowPolicy(loadedFabricBreadcrumbWindowPolicy); if (!options.preserveEditableForms) { setFabricBreadcrumbWindowPolicyForm({ currentWindowSeconds: String(loadedFabricBreadcrumbWindowPolicy.current_window_seconds || 1800), historyWindowSeconds: String(loadedFabricBreadcrumbWindowPolicy.history_window_seconds || 86400), }); } setFabricRecoveryPolicyForm({ hysteresisPenalty: String(loadedFabricRecoveryPolicy.hysteresis_penalty), promotionMinSamples: String(loadedFabricRecoveryPolicy.promotion_min_samples), demotionFailureThreshold: String(loadedFabricRecoveryPolicy.demotion_failure_threshold), demotionDropThreshold: String(loadedFabricRecoveryPolicy.demotion_drop_threshold), demotionSlowThreshold: String(loadedFabricRecoveryPolicy.demotion_slow_threshold), demotionRebuildEnabled: loadedFabricRecoveryPolicy.demotion_rebuild_enabled, demotionFencedEnabled: loadedFabricRecoveryPolicy.demotion_fenced_enabled, }); setQosPolicies(loadedQosPolicies); setEntryPoints(loadedEntryPoints); setEgressPools(loadedEgressPools); setVPNConnections(loadedVPNConnections); setTestingFlags(loadedTestingFlags); const diagnostics = await client.listVPNClientDiagnosticStatuses(clusterId); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setVPNClientDiagnostics(diagnostics); const selectedDiagnostic = diagnostics.find((item) => item.device_id === vpnDiagnosticDeviceId.trim()) || diagnostics[0] || null; setVPNClientDiagnostic(selectedDiagnostic); if (!vpnDiagnosticDeviceId.trim() && selectedDiagnostic) { setVPNDiagnosticDeviceId(selectedDiagnostic.device_id); localStorage.setItem(storageKeys.vpnDiagnosticDeviceId, selectedDiagnostic.device_id); } const [entryPointNodeEntries, egressPoolNodeEntries] = await Promise.all([ Promise.all(loadedEntryPoints.map(async (entryPoint) => [entryPoint.id, await client.listFabricEntryPointNodes(clusterId, entryPoint.id)] as const)), Promise.all(loadedEgressPools.map(async (pool) => [pool.id, await client.listFabricEgressPoolNodes(clusterId, pool.id)] as const)), ]); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setEntryPointNodesById(Object.fromEntries(entryPointNodeEntries)); setEgressPoolNodesById(Object.fromEntries(egressPoolNodeEntries)); const roleEntries = await Promise.all(loadedNodes.map(async (node) => [node.id, await client.listNodeRoles(clusterId, node.id)] as const)); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setRolesByNode(Object.fromEntries(roleEntries)); const desiredEntries = await Promise.all(loadedNodes.map(async (node) => [node.id, await client.listDesiredWorkloads(clusterId, node.id)] as const)); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setDesiredWorkloadsByNode(Object.fromEntries(desiredEntries)); const workloadEntries = await Promise.all( loadedNodes.map(async (node) => [node.id, await client.listWorkloadStatuses(clusterId, node.id)] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setWorkloadsByNode(Object.fromEntries(workloadEntries)); const heartbeatEntries = await Promise.all( loadedNodes.map(async (node) => [node.id, await client.listNodeHeartbeats(clusterId, node.id, 60)] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setHeartbeatsByNode(Object.fromEntries(heartbeatEntries)); const updatePlanEntries = await Promise.all( loadedNodes.map(async (node) => [node.id, await client.getNodeUpdatePlan(clusterId, node.id, { currentVersion: node.reported_version })] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setNodeUpdatePlansByNode(Object.fromEntries(updatePlanEntries)); const updateStatusEntries = await Promise.all( loadedNodes.map(async (node) => [node.id, await client.listNodeUpdateStatuses(clusterId, node.id, 80)] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setNodeUpdateStatusesByNode(Object.fromEntries(updateStatusEntries)); const telemetryEntries = await Promise.all( loadedNodes.map(async (node) => [node.id, await client.listNodeTelemetry(clusterId, node.id, 120)] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setTelemetryByNode(Object.fromEntries(telemetryEntries)); const syntheticMeshConfigEntries = await Promise.all( loadedNodes.map(async (node) => [node.id, await client.getNodeSyntheticMeshConfig(clusterId, node.id)] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setSyntheticMeshConfigsByNode(Object.fromEntries(syntheticMeshConfigEntries)); const leaseEntries = await Promise.all( loadedVPNConnections.map(async (connection) => [connection.id, await client.getActiveVPNLease(clusterId, connection.id)] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setVPNLeases(Object.fromEntries(leaseEntries)); const vpnPacketStatEntries = await Promise.all( loadedVPNConnections.map(async (connection) => [connection.id, await client.getVPNPacketStats(clusterId, connection.id)] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setVPNPacketStats(Object.fromEntries(vpnPacketStatEntries)); } async function loadFabricRebuildLedger(nextDeep = fabricRebuildLedgerDeep, nextFilters = fabricRebuildLedgerFilters) { if (!selectedClusterId) { return; } setLoading(true); setError(""); setNotice(""); try { const attempts = await client.listFabricServiceChannelRouteRebuildAttempts(selectedClusterId, { reporterNodeId: nextFilters.reporterNodeId || undefined, routeId: nextFilters.routeId || undefined, serviceClass: nextFilters.serviceClass || undefined, generation: nextFilters.generation || undefined, feedbackSource: nextFilters.feedbackSource || undefined, feedbackChannelId: nextFilters.feedbackChannelId || undefined, feedbackViolationStatus: nextFilters.feedbackViolationStatus || undefined, limit: nextDeep ? 20 : 10, offset: nextDeep ? nextFilters.offset : 0, enrichment: nextDeep ? "deep" : "summary", }); setFabricRebuildLedgerDeep(nextDeep); setFabricRebuildLedgerFilters(nextFilters); setFabricRebuildAttempts(attempts); setNotice(nextDeep ? "Deep rebuild ledger loaded." : "Fast rebuild ledger loaded."); } catch (err) { setError(err instanceof Error ? err.message : "Не удалось загрузить rebuild ledger."); } finally { setLoading(false); } } async function refreshFabricRebuildDiagnostics() { if (!selectedClusterId) { return; } const [health, silences, readiness, schemaStatus, snapshotHealth, leaseMaintenance, accessTelemetry, incidents, drilldownAudit, breadcrumbWindowPolicy] = await Promise.all([ client.getFabricServiceChannelRouteRebuildHealthSummary(selectedClusterId, { limit: 5 }), client.listFabricServiceChannelRouteRebuildAlertSilences(selectedClusterId), client.getFabricServiceChannelReadiness(selectedClusterId, { limit: 5 }), client.getFabricServiceChannelSchemaStatus(selectedClusterId), client.getFabricServiceChannelRebuildSnapshotMaintenanceHealth(selectedClusterId, { limit: 50, minAgeSeconds: 60, heartbeatThreshold: 2 }), client.getFabricServiceChannelLeaseMaintenance(selectedClusterId, { limit: 20, includeExpired: true }), client.getFabricServiceChannelAccessTelemetry(selectedClusterId, { limit: 20 }), client.listFabricServiceChannelRouteRebuildIncidents(selectedClusterId, { limit: 5 }), client.getFabricServiceChannelRebuildInvestigationBreadcrumbs(selectedClusterId, { limit: 20 }), client.getFabricServiceChannelBreadcrumbWindowPolicy(selectedClusterId), ]); setFabricRebuildHealth(health); setFabricRebuildSilences(silences); setFabricReadiness(readiness); setFabricSchemaStatus(schemaStatus); setFabricSnapshotHealth(snapshotHealth); setFabricLeaseMaintenance(leaseMaintenance); setFabricAccessTelemetry(accessTelemetry); setFabricRebuildIncidents(incidents); setFabricDrilldownAudit(drilldownAudit.events); setFabricDrilldownAuditSummary(drilldownAudit.summary || null); setFabricBreadcrumbWindowPolicy(breadcrumbWindowPolicy); setFabricBreadcrumbWindowPolicyForm({ currentWindowSeconds: String(breadcrumbWindowPolicy.current_window_seconds || 1800), historyWindowSeconds: String(breadcrumbWindowPolicy.history_window_seconds || 86400), }); } async function warmupFabricRebuildSnapshots() { if (!selectedClusterId) { return; } try { setLoading(true); const warmup = await client.warmupFabricServiceChannelRebuildSnapshots(selectedClusterId, { limit: 10, staleAfterSeconds: 60 }); setFabricSnapshotWarmup(warmup); await refreshFabricRebuildDiagnostics(); setNotice(`Snapshot warmup: warmed ${warmup.warmed_count}, fresh ${warmup.already_fresh_count}, errors ${warmup.error_count}.`); } catch (err) { setError(err instanceof Error ? err.message : "Не удалось прогреть rebuild snapshots."); } finally { setLoading(false); } } async function cleanupFabricServiceChannelLeases() { if (!selectedClusterId) { return; } try { setLoading(true); const result = await client.cleanupFabricServiceChannelLeases(selectedClusterId, { limit: 100 }); setFabricLeaseMaintenance(result); setNotice(`Service-channel lease cleanup: deleted ${result.deleted_expired_count || 0}, active ${result.active_count}, expired ${result.expired_count}.`); } catch (err) { setError(err instanceof Error ? err.message : "Не удалось очистить service-channel leases."); } finally { setLoading(false); } } async function openFabricRebuildIncidentDeepLedger(incident: FabricServiceChannelRouteRebuildIncident) { const filters = { reporterNodeId: incident.reporter_node_id, routeId: incident.route_id, serviceClass: incident.service_class, generation: incident.generation || "", feedbackSource: "", feedbackChannelId: incident.channel_id || "", feedbackViolationStatus: "", offset: 0, }; await client.recordFabricServiceChannelRouteRebuildInvestigation(selectedClusterId, { reporterNodeId: incident.reporter_node_id, routeId: incident.route_id, serviceClass: incident.service_class, generation: incident.generation || "", guardStatus: incident.guard_status, incidentId: incident.fingerprint, }); const drilldownAudit = await client.getFabricServiceChannelRebuildInvestigationBreadcrumbs(selectedClusterId, { limit: 20 }); setFabricDrilldownAudit(drilldownAudit.events); setFabricDrilldownAuditSummary(drilldownAudit.summary || null); setFabricRebuildLedgerFilters(filters); await loadFabricRebuildLedger(true, filters); } function fabricRebuildIncidentsForFeedbackBreakdown(item: FabricServiceChannelRouteRebuildFeedbackHealthBreakdown) { const reporterNodeIDs = new Set(item.affected_reporter_node_ids || []); const routeIDs = new Set(item.affected_route_ids || []); return fabricRebuildIncidents.filter((incident) => { const channelMatches = !item.feedback_channel_id || incident.channel_id === item.feedback_channel_id; const reporterMatches = reporterNodeIDs.size === 0 || reporterNodeIDs.has(incident.reporter_node_id); const routeMatches = routeIDs.size === 0 || routeIDs.has(incident.route_id); return channelMatches && reporterMatches && routeMatches; }); } function fabricFeedbackBreakdownForAuditEvent(event: AuditEvent) { const payload = objectField(event.payload) || {}; const feedbackSource = stringField(payload, "feedback_source", ""); const feedbackChannelID = stringField(payload, "feedback_channel_id", ""); const feedbackViolationStatus = stringField(payload, "feedback_violation_status", ""); const reporterNodeID = stringField(payload, "reporter_node_id", ""); const routeID = stringField(payload, "route_id", ""); if (!feedbackSource && !feedbackChannelID && !feedbackViolationStatus) { return null; } return ( (fabricRebuildHealth?.feedback_breakdowns || []).find((item) => { if (feedbackSource && item.feedback_source !== feedbackSource) { return false; } if (feedbackChannelID && item.feedback_channel_id !== feedbackChannelID) { return false; } if (feedbackViolationStatus && item.feedback_violation_status !== feedbackViolationStatus) { return false; } if (reporterNodeID && !(item.affected_reporter_node_ids || []).includes(reporterNodeID)) { return false; } if (routeID && !(item.affected_route_ids || []).includes(routeID)) { return false; } return true; }) || null ); } function fabricRebuildIncidentForAuditEvent(event: AuditEvent) { const payload = objectField(event.payload) || {}; const reporterNodeID = stringField(payload, "reporter_node_id", ""); const routeID = stringField(payload, "route_id", event.target_type === "fabric_service_channel_route_rebuild_incident" ? event.target_id || "" : ""); const serviceClass = stringField(payload, "service_class", ""); const generation = stringField(payload, "generation", ""); const guardStatus = stringField(payload, "guard_status", ""); return ( fabricRebuildIncidents.find((incident) => { if (reporterNodeID && incident.reporter_node_id !== reporterNodeID) { return false; } if (routeID && incident.route_id !== routeID) { return false; } if (serviceClass && incident.service_class !== serviceClass) { return false; } if (generation && incident.generation !== generation) { return false; } if (guardStatus && incident.guard_status !== guardStatus) { return false; } return Boolean(reporterNodeID || routeID || serviceClass || generation || guardStatus); }) || null ); } async function openFabricRebuildFeedbackBreakdownLedger(item: FabricServiceChannelRouteRebuildFeedbackHealthBreakdown) { const filters = { ...defaultFabricRebuildLedgerFilters, feedbackSource: item.feedback_source || "", feedbackChannelId: item.feedback_channel_id || "", feedbackViolationStatus: item.feedback_violation_status || "", offset: 0, }; await client.recordFabricServiceChannelRouteRebuildInvestigation(selectedClusterId, { reporterNodeId: (item.affected_reporter_node_ids || [])[0] || "", routeId: (item.affected_route_ids || [])[0] || "", feedbackSource: item.feedback_source || "", feedbackChannelId: item.feedback_channel_id || "", feedbackViolationStatus: item.feedback_violation_status || "", drilldownSource: "rebuild_health_feedback_breakdown", reason: "operator opened rebuild-health feedback breakdown ledger", }); const drilldownAudit = await client.getFabricServiceChannelRebuildInvestigationBreadcrumbs(selectedClusterId, { limit: 20 }); setFabricDrilldownAudit(drilldownAudit.events); setFabricDrilldownAuditSummary(drilldownAudit.summary || null); setActiveView("fabric"); setFabricRebuildLedgerFilters(filters); await loadFabricRebuildLedger(true, filters); } async function silenceFabricRebuildIncident(incident: FabricServiceChannelRouteRebuildIncident) { await client.silenceFabricServiceChannelRouteRebuildAlert(selectedClusterId, { incidentSource: incident.incident_source || "", channelId: incident.channel_id || "", reporterNodeId: incident.reporter_node_id, routeId: incident.route_id, guardStatus: incident.guard_status || "unknown", generation: incident.generation || "", reason: "operator acknowledged rebuild incident", ttlSeconds: 21600, }); await refreshFabricRebuildDiagnostics(); } async function unsilenceFabricRebuildAlert(silence: FabricServiceChannelRouteRebuildAlertSilence) { await client.unsilenceFabricServiceChannelRouteRebuildAlert( selectedClusterId, silence.id, "operator removed rebuild alert silence", ); await refreshFabricRebuildDiagnostics(); } function clearClusterScope() { setNodes([]); setNodeGroups([]); setJoinRequests([]); setJoinTokens([]); setReleaseVersions([]); setNodeUpdatePlansByNode({}); setAuthority(null); setRolesByNode({}); setDesiredWorkloadsByNode({}); setWorkloadsByNode({}); setHeartbeatsByNode({}); setNodeUpdateStatusesByNode({}); setTelemetryByNode({}); setMeshLinks([]); setRouteIntents([]); setSyntheticMeshConfigsByNode({}); setFabricRouteFeedback([]); setFabricRebuildAttempts([]); setFabricRebuildHealth(null); setFabricRebuildSilences([]); setFabricReadiness(null); setFabricSchemaStatus(null); setFabricSnapshotWarmup(null); setFabricRebuildIncidents([]); setFabricRebuildLedgerDeep(false); setFabricRebuildLedgerFilters(defaultFabricRebuildLedgerFilters); setQosPolicies([]); setEntryPoints([]); setEntryPointNodesById({}); setEgressPools([]); setEgressPoolNodesById({}); setTestingFlags([]); setVPNConnections([]); setVPNLeases({}); setVPNPacketStats({}); setVPNClientDiagnostics([]); setVPNClientDiagnostic(null); setOrganizations([]); setUsers([]); setResources([]); setMembershipsByOrg({}); setAudit([]); setFabricDrilldownAudit([]); setFabricDrilldownAuditSummary(null); } async function runAction(action: () => Promise, success: string) { setLoading(true); setError(""); setNotice(""); try { await action(); setNotice(success); await refreshAll(); } catch (err) { setError(err instanceof Error ? err.message : "Действие не выполнено."); } finally { setLoading(false); } } async function refreshVPNClientDiagnostic() { if (!selectedClusterId) { setVPNClientDiagnostic(null); return; } const diagnostics = await client.listVPNClientDiagnosticStatuses(selectedClusterId); setVPNClientDiagnostics(diagnostics); const selectedDeviceId = vpnDiagnosticDeviceId.trim() || diagnostics[0]?.device_id || ""; if (selectedDeviceId) { localStorage.setItem(storageKeys.vpnDiagnosticDeviceId, selectedDeviceId); setVPNDiagnosticDeviceId(selectedDeviceId); } const diagnostic = diagnostics.find((item) => item.device_id === selectedDeviceId) || (selectedDeviceId ? await client.getVPNClientDiagnosticStatus(selectedClusterId, selectedDeviceId) : null); setVPNClientDiagnostic(diagnostic); setNotice(diagnostic ? "Диагностика VPN-клиента обновлена." : "Диагностика VPN-клиента не найдена."); } async function sendVPNDiagnosticCommand(command: Record, label: string) { if (!selectedClusterId) { setError("Выбери кластер перед отправкой команды."); return; } const deviceId = vpnDiagnosticDeviceId.trim(); if (!deviceId) { setError("Укажи Android device id или выбери найденный клиент."); return; } setLoading(true); setError(""); setNotice(""); try { const queued = await client.enqueueVPNClientDiagnosticCommand(selectedClusterId, deviceId, command); setLastVPNDiagnosticCommand(queued); setNotice(`${label}: команда поставлена в очередь. Клиент заберет ее через диагностический канал.`); window.setTimeout(() => { void refreshVPNClientDiagnostic(); }, 3500); } catch (err) { setError(err instanceof Error ? err.message : "Команда VPN-клиенту не отправлена."); } finally { setLoading(false); } } async function handleLogin() { setLoading(true); setError(""); setNotice(""); try { const result = await authClient.login({ email: loginForm.email, password: loginForm.password, deviceLabel: loginForm.deviceLabel, trustDevice: loginForm.trustDevice, }); const nextSession = normalizeAuthResult(result); if (!nextSession.userId || !nextSession.authSessionId) { throw new Error("Ответ входа не содержит пользователя или сессию."); } const accessClient = new AdminApiClient({ baseUrl, actorUserId: nextSession.userId }); let nextMode: ConsoleMode = "admin"; try { await accessClient.listClusterSummaries(); nextMode = "admin"; } catch { try { const [loadedOrganizations, loadedResources] = await Promise.all([accessClient.listOrganizations(), accessClient.listResources()]); setOrganizations(loadedOrganizations); setResources(loadedResources); if (loadedOrganizations[0]?.id) { setOrganizationId(loadedOrganizations[0].id); } const membershipEntries = await Promise.all( loadedOrganizations.map(async (org) => [org.id, await accessClient.listOrganizationMemberships(org.id)] as const), ); setMembershipsByOrg(Object.fromEntries(membershipEntries)); nextMode = "user"; } catch { try { await authClient.revokeAuthSession({ userId: nextSession.userId, authSessionId: nextSession.authSessionId, reason: "user_portal_access_denied", }); } catch { // Authentication succeeded, but no accessible workspace was found. } throw new Error(t.accessDenied); } } setRememberSession(loginForm.rememberMe); persistSession(nextSession, loginForm.rememberMe); setSession(nextSession); setActorUserId(nextSession.userId); setLoginForm((previous) => ({ ...previous, email: nextSession.email, password: "" })); setSessionRefreshedAt(new Date().toISOString()); setConsoleMode(nextMode); setNotice(`${t.signedInAs}: ${nextSession.email}`); } catch (err) { setError(err instanceof Error ? err.message : "Вход не выполнен."); } finally { setLoading(false); } } async function handleBootstrapOwner() { setLoading(true); setError(""); setNotice(""); try { let activationPayload: unknown; if (installationStatus?.strict_authority) { if (!bootstrapForm.activationPayload.trim() || !bootstrapForm.activationSignature.trim()) { throw new Error(t.bootstrapText); } activationPayload = JSON.parse(bootstrapForm.activationPayload); } const result = await authClient.bootstrapOwner({ email: bootstrapForm.email, password: bootstrapForm.password, activationPayload, activationSignature: bootstrapForm.activationSignature, }); setInstallationStatus(result.installation); setLoginForm({ ...loginForm, email: bootstrapForm.email, password: bootstrapForm.password }); setNotice(t.ownerCreated); } catch (err) { setError(err instanceof Error ? err.message : "Создание владельца не выполнено."); } finally { setLoading(false); } } async function handleLogout() { const previous = session; setSession(null); setRememberSession(false); setSessionRefreshedAt(""); persistSession(null); setConsoleMode(null); setActorUserId(""); setClusters([]); setClusterSummaries([]); clearClusterScope(); setAllNodesByCluster({}); setSelectedClusterId(""); if (previous?.userId && previous.authSessionId) { try { await authClient.revokeAuthSession({ userId: previous.userId, authSessionId: previous.authSessionId, reason: "platform_owner_console_logout", }); } catch { // The local session is already cleared. Backend revoke failure should not trap the operator in the UI. } } } async function switchCluster(clusterId: string) { setSelectedClusterId(clusterId); clearClusterScope(); setLoading(true); setError(""); setNotice(""); try { await loadClusterScope(clusterId); } catch (err) { setError(err instanceof Error ? err.message : "Не удалось загрузить кластер."); } finally { setLoading(false); } } const pendingJoinCount = joinRequests.filter((request) => request.status === "pending").length; const healthyNodeCount = nodes.filter((node) => node.health_status === "healthy").length; const riskyNodeCount = nodes.filter((node) => node.health_status !== "healthy" || node.membership_status !== "active").length; const activeRoleCount = Object.values(rolesByNode).flat().filter((role) => role.status === "active").length; const platformTestingFlag = testingFlags.find((flag) => flag.scope_type === "platform" && !flag.scope_id) || null; const organizationTestingFlag = testingFlags.find((flag) => flag.scope_type === "organization" && flag.scope_id === testingOrgId && (!flag.cluster_id || flag.cluster_id === selectedClusterId)) || null; const syntheticMeshConfigs = Object.values(syntheticMeshConfigsByNode); const syntheticConfigEnabledCount = syntheticMeshConfigs.filter((config) => config.enabled).length; const syntheticRouteCount = syntheticMeshConfigs.reduce((sum, config) => sum + config.routes.length, 0); const syntheticPeerEndpointCount = syntheticMeshConfigs.reduce((sum, config) => sum + Object.keys(config.peer_endpoints || {}).length, 0); const syntheticCandidateCount = syntheticMeshConfigs.reduce((sum, config) => sum + countPeerEndpointCandidates(config), 0); const syntheticPeerDirectoryCount = syntheticMeshConfigs.reduce((sum, config) => sum + (config.peer_directory?.length ?? 0), 0); const syntheticRecoverySeedCount = syntheticMeshConfigs.reduce((sum, config) => sum + (config.recovery_seeds?.length ?? 0), 0); const productionForwardingConfigCount = syntheticMeshConfigs.filter((config) => config.production_forwarding).length; const routeIntentActive = routeIntents.filter((item) => routeIntentLifecycle(item) === "active"); const routeIntentExpired = routeIntents.filter((item) => routeIntentLifecycle(item) === "expired"); const routeIntentDisabled = routeIntents.filter((item) => routeIntentLifecycle(item) === "disabled"); const fabricRouteFeedbackVisible = fabricRouteFeedback.filter((item) => { const expiresAt = Date.parse(item.expires_at || ""); const retryUntil = Date.parse(item.retry_cooldown_until || ""); return (Number.isFinite(expiresAt) && expiresAt > Date.now()) || (Number.isFinite(retryUntil) && retryUntil > Date.now()); }); const fabricFeedbackFenced = fabricRouteFeedbackVisible.filter((item) => item.feedback_status === "fenced"); const fabricFeedbackDegraded = fabricRouteFeedbackVisible.filter((item) => item.feedback_status === "degraded"); const fabricFeedbackHealthy = fabricRouteFeedbackVisible.filter((item) => item.feedback_status === "healthy"); const fabricFeedbackRecovered = fabricRouteFeedbackVisible.filter((item) => item.recovery_state === "recovered" || item.recovery_hysteresis_active); const fabricFeedbackPromoted = fabricRouteFeedbackVisible.filter((item) => item.recovery_promoted); const fabricFeedbackDemoted = fabricRouteFeedbackVisible.filter((item) => item.recovery_demoted); const fabricFeedbackRetryCooldown = fabricRouteFeedbackVisible.filter((item) => item.feedback_status === "operator_retry_cooldown" || item.retry_cooldown_until); const fabricRouteDecisions = syntheticMeshConfigs.flatMap((config) => config.route_path_decisions?.decisions || []); const fabricNoAlternateDecisions = fabricRouteDecisions.filter((decision) => decision.decision_source === "service_channel_feedback_no_alternate"); const fabricReplacementDecisions = fabricRouteDecisions.filter((decision) => decision.decision_source === "service_channel_feedback_replacement"); const fabricRebuildDecisions = fabricRouteDecisions.filter((decision) => decision.rebuild_status); const fabricRebuildAppliedDecisions = fabricRebuildDecisions.filter((decision) => decision.rebuild_status === "applied"); const fabricRebuildAppliedAttempts = fabricRebuildAttempts.filter((attempt) => attempt.rebuild_status === "applied"); const fabricRebuildPendingAttempts = fabricRebuildAttempts.filter((attempt) => attempt.rebuild_status && attempt.rebuild_status !== "applied"); const fabricRebuildGuardAlerts = fabricRebuildAttempts.filter((attempt) => attempt.guard_severity === "bad"); const fabricRecoveryHysteresisDecisions = fabricRouteDecisions.filter((decision) => (decision.score_reasons || []).includes("service_channel_recovery_hysteresis")); const fabricRecoveryPromotedDecisions = fabricRouteDecisions.filter((decision) => (decision.score_reasons || []).includes("service_channel_recovery_promoted")); const fabricRecoveryDemotedDecisions = fabricRouteDecisions.filter((decision) => (decision.score_reasons || []).includes("service_channel_recovery_demoted")); const bootstrapRequired = installationStatus?.bootstrapped === false; const bootstrapBlocked = bootstrapRequired && !installationStatus?.strict_authority && !installationStatus?.insecure_bootstrap_allowed; const sessionModeLabel = consoleMode === "admin" ? t.sessionModeAdmin : t.sessionModeUser; if (!session) { return (
{installationStatus && (

{installationStatus.bootstrapped ? t.installationLocked : t.bootstrapTitle}

{installationStatus.root_fingerprint && }
)} {bootstrapRequired ? (

{t.bootstrapTitle}

{bootstrapBlocked ? t.insecureBootstrapDisabled : t.bootstrapText}

{installationStatus?.strict_authority && ( <>