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, FabricServiceChannelAccessTelemetry, FabricServiceChannelBreadcrumbWindowPolicy, FabricServiceChannelRecoveryPolicy, FabricServiceChannelLeaseMaintenance, FabricServiceChannelReadiness, FabricServiceChannelRouteFeedbackObservation, FabricServiceChannelRouteRebuildAlertSilence, FabricServiceChannelRouteRebuildAttempt, FabricServiceChannelRouteRebuildFeedbackHealthBreakdown, FabricServiceChannelRouteRebuildHealthSummary, FabricServiceChannelRouteRebuildIncident, FabricServiceChannelRebuildSnapshotMaintenanceHealth, FabricServiceChannelRebuildSnapshotWarmup, FabricServiceChannelSchemaStatus, FabricTestingFlag, InstallationStatus, JoinRequest, InstallJoinBundle, MeshLink, MeshRouteIntent, NodeHeartbeat, NodeBridgeReplayPlan, NodeJoinToken, NodeSyntheticMeshConfig, NodeTelemetryObservation, NodeUpdatePlan, NodeUpdateStatus, NodeWorkloadDesiredState, Organization, OrganizationAdminSummary, OrganizationMembership, QoSPolicy, ReleaseVersion, Resource, RoleAssignment, StaleNodeRiskReport, UserAccount, VPNClientDiagnosticCommand, VPNClientDiagnosticStatus, VPNConnection, VPNConnectionLease, VPNPacketStats, WorkloadStatus, } from "./types"; const storageKeys = { actorUserId: "rap.webAdmin.actorUserId", auth: "rap.webAdmin.auth", language: "rap.webAdmin.language", vpnDiagnosticDeviceId: "rap.webAdmin.vpnDiagnosticDeviceId", }; 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 fabricNodeRoleOptions = [ "core-mesh", "entry-node", "relay-node", "update-cache", ]; const serviceWorkloadOptions = [ "public-ingress", "admin-ingress", "core-mesh", "rdp-worker", "vnc-worker", "vpn-exit", "vpn-connector", "vpn-client", "ipv4-egress", "file-storage-cache", "update-cache", "video-relay", ]; const roleOptions = fabricNodeRoleOptions; const roleDisplayNames: Record = { "public-ingress": "Public HTTPS ingress", "admin-ingress": "Admin HTTPS ingress", "entry-node": "Fabric entry", "relay-node": "Fabric relay", "core-mesh": "Fabric core", "rdp-worker": "RDP worker", "vnc-worker": "VNC worker", "vpn-exit": "VPN exit", "vpn-connector": "VPN connector", "vpn-client": "VPN client node", "ipv4-egress": "IPv4 egress", "file-storage-cache": "Config store", "update-cache": "Update store", "video-relay": "Video relay", }; const capabilityKeysByRole: Record = { "public-ingress": ["can_accept_client_ingress", "fabric_service_channel_runtime"], "admin-ingress": ["can_accept_client_ingress", "fabric_service_channel_runtime"], "entry-node": ["can_accept_client_ingress"], "relay-node": ["mesh_rendezvous_relay_quic_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"], "vpn-client": ["can_run_vpn_client", "fabric_service_channel_required"], "ipv4-egress": ["can_egress_internet", "fabric_service_channel_required"], "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: "functions", ru: "Функции", en: "Functions" }, { 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"; const navGroups: Array<{ label: Record; views: ViewId[] }> = [ { label: { ru: "Ферма", en: "Farm" }, views: ["command", "clusters", "cluster-settings", "nodes", "enrollment", "functions", "fabric"] }, { label: { ru: "Сервисы", en: "Services" }, views: ["workloads", "vpn", "servers"] }, { label: { ru: "Доступ", en: "Access" }, views: ["org-safe", "audit"] }, ]; const viewsById = Object.fromEntries(views.map((view) => [view.id, view])) as Record; 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; fabricListenAddr: string; fabricListenPortMode: string; fabricListenAutoPortStart: number; fabricListenAutoPortEnd: number; meshAdvertiseEndpoint: string; meshAdvertiseEndpoints: string; meshAdvertiseTransport: string; meshConnectivityMode: string; meshNATType: string; meshRegion: 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:0.2.353-sticky-fabric-control", dockerContainerName: "", dockerNetwork: "host", windowsStartupMode: "auto", windowsInstallDir: "", windowsNodeAgentSHA256: "", linuxInstallDir: "", linuxNodeAgentSHA256: "", fabricListenAddr: "", fabricListenPortMode: "auto", fabricListenAutoPortStart: 19131, fabricListenAutoPortEnd: 19231, meshAdvertiseEndpoint: "", meshAdvertiseEndpoints: "", meshAdvertiseTransport: "direct_quic", meshConnectivityMode: "private_lan", meshNATType: "none", meshRegion: "docker-test", artifactEndpoints: "", dockerImageArtifactSHA256: "", pullImage: false, replace: true, syntheticRuntime: false, }; 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: "Пароль", language: "Язык", deviceLabel: "Устройство", rememberMe: "Запомнить меня", trustDevice: "Доверять этому устройству", signIn: "Войти", signingIn: "Вход...", logout: "Выйти", profile: "Профиль", refresh: "Обновить", refreshing: "Обновление...", autoRefresh: "Автообновление", lastRefresh: "Данные обновлены", activeCluster: "Активный кластер", slugLabel: "Технический код", slugHelp: "Slug — постоянный короткий технический идентификатор кластера для URL, скриптов, логов и интеграций. Его лучше не менять после создания.", clusterCatalog: "Каталог кластеров", clusterCatalogText: "Список реальных кластеров из Control/API слоя. Выберите активный кластер или раскройте карточку для подробностей.", makeActive: "Сделать активным", openSettings: "Открыть настройки", selected: "Выбран", createCluster: "Создать кластер", clusterDetails: "Подробнее", consoleTitle: "Панель владельца платформы", boundary: "Панель является сервисным интерфейсом управления. Внутри фермы transport остается QUIC/UDP-only.", noLoginError: "Войдите как владелец продукта или администратор платформы, чтобы загрузить панель.", accessDenied: "Доступ к этой панели запрещен.", sessionMode: "Режим сессии", sessionModeAdmin: "Админ", sessionModeUser: "Пользователь", sessionRefreshedAt: "Сессия обновлена", emptyLiveTitle: "Кластер пока пустой", emptyLiveText: "Это реальные данные, не заглушка: в выбранном кластере ещё нет одобренных node-agent узлов. Создайте join token, запустите rap-node-agent и подтвердите join request.", realDataNote: "Показываются только данные из PostgreSQL и Control/API слоя. Если значения нулевые, значит соответствующих узлов, ролей или сервисов пока нет.", 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: "Только функции инфраструктуры фермы: fabric core, вход, relay, обновления и config store. Сервисные workloads на этом уровне не назначаются.", rolePermission: "Разрешение", permissionGranted: "разрешено", permissionDenied: "нет разрешения", organizationScopeForEnable: "Область назначения", clusterWideRolePlaceholder: "функции узла всегда назначаются на весь кластер", desiredRuntime: "Желаемое состояние", observedRuntime: "Фактически", enableFunction: "Включить функцию", disableFunction: "Выключить функцию", close: "Закрыть", nodeBriefList: "Краткий список узлов", noActiveClusterMembership: "Узел не входит в активный кластер", nodeBriefListHelp: "Список сгруппирован деревом активного кластера. Полные сведения, управление, функции фермы, сервисы и статистика открываются из строки узла.", nodeSearchPlaceholder: "имя, ключ, кластер, статус", nodeGroupInventoryText: "Группы — это кластерная инвентарная структура. Перенос узла в группу меняет только его размещение внутри активного кластера, не функции и не членство.", nodeGroupCreated: "Группа узлов создана.", noNodesTitle: "Нет узлов", noNodesByFilter: "По текущему фильтру узлы не найдены.", cancel: "Отмена", alreadyMember: "Уже в активном кластере", revokedMembership: "Участие отозвано", addNode: "Подключить узел", addNodeText: "Подключение существующего физического узла к активному кластеру выполняется из списка узлов: включите общий режим и нажмите «Подключить к активному кластеру».", joinTokenTitle: "Создать новый узел", joinTokenText: "Панель выпускает одноразовый install token и signed join bundle. Дальше оператор запускает bundle-first install, узел входит в ферму через QUIC fabric и отправляет заявку на approval.", 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", fabricNodeLayer: "Узлы кластера", observedPeerLinks: "Наблюдаемые связи", placementIntent: "управляющее назначение", endpointName: "Название", publicEndpoint: "Публичный адрес", endpointType: "Тип входа", description: "Описание", routeScope: "Область маршрутов JSON", endpointNodes: "Назначенные узлы", assignEndpointNode: "Назначить узел", selectNode: "Выберите узел", assignedNodesEmpty: "Узлы пока не назначены", addressNotSet: "адрес не задан", descriptionNotSet: "описание не задано", servicePlacement: "Размещение сервисов", trafficFlow: "Потоки между узлами", organizationTestFlag: "Тестирование организации", organizationId: "ID организации", saveOrganizationFlag: "Сохранить флаг организации", noLinks: "Связей пока нет", recentHeartbeats: "Последние heartbeat", memory: "Память", cpu: "Процессор", processes: "Процессы", }, en: { productOwner: "Product Owner", controlPlane: "Fabric control", 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", 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/API layer. 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: "The panel is a service management interface. Internal farm transport remains QUIC/UDP-only.", 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 and Control/API data is shown. Zero values mean the corresponding nodes, functions, 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. Farm functions 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 farm functions 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. The functions below are assigned only in this cluster.", connectWithRoles: "Connect with functions", nodeDetails: "Details", manageNode: "Configure", nodeFunctions: "Node functions", nodeFunctionsText: "Farm infrastructure only: fabric core, entry, relay, update store, and config store. Service workloads are not assigned at node level.", rolePermission: "Permission", permissionGranted: "granted", permissionDenied: "not allowed", organizationScopeForEnable: "Assignment scope", clusterWideRolePlaceholder: "node functions are always cluster-wide", 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, farm functions, 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 functions 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 node", joinTokenText: "The panel issues a one-time install token and a signed join bundle. Then the operator runs the bundle-first install, the node joins through QUIC fabric, and the platform owner approves the request.", 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: "Farm node functions", 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: "Farm functions", desiredServices: "Desired services", observedServices: "Observed services", noRoles: "No functions yet", noServices: "No services yet", manageInCluster: "Manage in cluster", rolesAndServices: "Functions and services", links: "Links", fabricMap: "Fabric traffic map", fabricNodeLayer: "Cluster nodes", observedPeerLinks: "Observed links", placementIntent: "control/API placement", endpointName: "Name", publicEndpoint: "Public endpoint", endpointType: "Entry type", description: "Description", routeScope: "Route scope JSON", endpointNodes: "Assigned nodes", assignEndpointNode: "Assign node", selectNode: "Select node", assignedNodesEmpty: "No nodes assigned 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 [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 [staleNodeRiskReport, setStaleNodeRiskReport] = useState(null); const [bridgeReplayPlansByNode, setBridgeReplayPlansByNode] = 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 [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 [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 [auditFocusNodeId, setAuditFocusNodeId] = useState(""); const [auditFocusNodeLabel, setAuditFocusNodeLabel] = 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 [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 [lastJoinBundles, setLastJoinBundles] = useState<{ docker?: InstallJoinBundle | null; linux_binary?: InstallJoinBundle | null; windows_service?: InstallJoinBundle | null; }>({}); const [authorityForm, setAuthorityForm] = useState({ authorityState: "authoritative", mutationMode: "normal", notes: "" }); 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 [fabricListenerDrafts, setFabricListenerDrafts] = useState< Record< string, { listenAddr: string; mode: string; autoRange: string; advertiseEndpoint: string; endpointCandidates: string; advertiseTransport: string; connectivity: string; nat: string; region: string; } > >({}); const [nodeTestingDrafts, setNodeTestingDrafts] = useState>({}); const [testingOrgId, setTestingOrgId] = useState(""); const [testingOrgDraft, setTestingOrgDraft] = useState({ telemetry: true, links: true }); const [workloadForm, setWorkloadForm] = useState({ nodeId: "", serviceType: "rdp-worker", 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 [standardGuardSmokeResult, setstandardGuardSmokeResult] = useState(""); const [standardGuardSmokeCheckedAt, setstandardGuardSmokeCheckedAt] = useState(""); 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-vpn-latest-release.apk"; const [androidClientLatestPath, setAndroidClientLatestPath] = useState(androidClientDefaultLatestFilename); const client = useMemo(() => new AdminApiClient({ actorUserId }), [actorUserId]); const authClient = useMemo(() => new AdminApiClient({ actorUserId: "" }), []); 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(() => (typeof window === "undefined" ? "" : window.location.origin), []); 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 activeJoinBundle = useMemo(() => { if (joinTokenForm.installMode === "windows_service") { return lastJoinBundles.windows_service || null; } if (joinTokenForm.installMode === "linux_binary") { return lastJoinBundles.linux_binary || null; } return lastJoinBundles.docker || null; }, [joinTokenForm.installMode, lastJoinBundles]); 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 nodeInventoryByNodeId = useMemo( () => Object.fromEntries(allNodeInventory.map((entry) => [entry.node.id, entry])), [allNodeInventory], ); 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-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 standardCleanupBlockedAuditEvents = useMemo( () => audit .filter((event) => event.event_type === "fabric_standard_cleanup.blocked") .slice(0, 4), [audit], ); const visibleAuditEvents = useMemo( () => auditFocusNodeId ? audit.filter((event) => auditEventMatchesNode(event, auditFocusNodeId)) : audit, [audit, auditFocusNodeId], ); const focusBlockerNode = useCallback( (nodeId: string, mode: "details" | "manage") => { const entry = nodeInventoryByNodeId[nodeId]; if (!entry) { return; } setActiveView("nodes"); setNodeViewScope("all"); setNodeGroupFilterId(""); setAllNodeSearch(entry.node.name || entry.node.node_key); setNodeInfoDialog(entry); setNodeInfoMode(mode); }, [nodeInventoryByNodeId], ); const focusNodeAuditTrail = useCallback( (nodeId: string) => { const entry = nodeInventoryByNodeId[nodeId]; setAuditFocusNodeId(nodeId); setAuditFocusNodeLabel(entry?.node.name || entry?.node.node_key || nodeId); setActiveView("audit"); }, [nodeInventoryByNodeId], ); 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({ 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, 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.language, language); if (session) { localStorage.setItem(`${storageKeys.language}.${session.userId}`, language); } if (!session || !rememberSession) { localStorage.removeItem(storageKeys.auth); localStorage.removeItem(storageKeys.actorUserId); } }, [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, loadedStaleNodeRiskReport, loadedAuthority, loadedAudit, loadedFabricDrilldownAuditResult, loadedMeshLinks, loadedRouteIntents, loadedFabricRouteFeedback, loadedFabricRebuildAttempts, loadedFabricRebuildHealth, loadedFabricRebuildSilences, loadedFabricReadiness, loadedFabricSchemaStatus, loadedFabricSnapshotHealth, loadedFabricLeaseMaintenance, loadedFabricAccessTelemetry, loadedFabricRebuildIncidents, loadedFabricRecoveryPolicy, loadedFabricBreadcrumbWindowPolicy, loadedQosPolicies, 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.getStaleNodeRiskReport(clusterId), 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.listVPNConnections(clusterId), client.listFabricTestingFlags(), ]); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setNodes(loadedNodes); setNodeGroups(loadedNodeGroups); setJoinRequests(loadedJoinRequests); setJoinTokens(loadedJoinTokens); setReleaseVersions(loadedReleaseVersions); setStaleNodeRiskReport(loadedStaleNodeRiskReport); 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); 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 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 replayReadyNodeIds = loadedStaleNodeRiskReport.nodes .filter((node) => node.recovery_bridge_replay_ready) .map((node) => node.node_id); if (replayReadyNodeIds.length > 0) { const bridgeReplayEntries = await Promise.all( replayReadyNodeIds.map(async (nodeId) => [nodeId, await client.getNodeBridgeReplayPlan(clusterId, nodeId)] as const), ); if (requestSeq !== clusterScopeRequestSeq.current) { return; } setBridgeReplayPlansByNode(Object.fromEntries(bridgeReplayEntries)); } else { setBridgeReplayPlansByNode({}); } 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([]); setStaleNodeRiskReport(null); setBridgeReplayPlansByNode({}); 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([]); 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 runFabricStandardCleanupGuardSmoke() { if (!selectedClusterId || !staleNodeRiskReport || staleNodeRiskReport.summary.blocked_nodes < 1) { setstandardGuardSmokeResult("Guard smoke сейчас недоступен: в отчете нет blocker-узлов для controlled blocked-check."); setstandardGuardSmokeCheckedAt(new Date().toISOString()); return; } setLoading(true); setError(""); setNotice(""); setstandardGuardSmokeResult(""); try { const smokeVersion = `0.2.402-guard-smoke-${Date.now()}`; await client.createReleaseVersion(selectedClusterId, { product: "rap-node-agent", version: smokeVersion, channel: "stable", status: "active", changelog: "UI smoke check for fabric standard cleanup guard", artifacts: [ { os: "linux", arch: "amd64", installType: "docker", kind: "image", url: "fabric-artifact://guard-smoke/rap-node-agent.tar", sha256: "sha256-guard-smoke", sizeBytes: 123, metadata: {}, }, ], }); setstandardGuardSmokeResult( "Smoke unexpectedly succeeded. Guard should have blocked breaking release creation while stale recovery-risk nodes remain.", ); } catch (err) { setstandardGuardSmokeResult(err instanceof Error ? err.message : "Guard smoke failed with a non-Error response."); } finally { setstandardGuardSmokeCheckedAt(new Date().toISOString()); 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({ 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: "public_ingress_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 freshNodeCount = nodes.filter((node) => nodeSignalFreshness(node, (heartbeatsByNode[node.id] || [])[0]).state === "fresh").length; const staleSignalNodeCount = nodes.filter((node) => { const state = nodeSignalFreshness(node, (heartbeatsByNode[node.id] || [])[0]).state; return state === "stale" || state === "no_signal"; }).length; const rescueHoldNodeCount = nodes.filter((node) => nodeUpdaterSummary(nodeUpdateStatusesByNode[node.id] || []).reason === "fabric_executor_upgrade_required").length; const activeRoleCount = Object.values(rolesByNode) .flat() .filter((role) => role.status === "active" && isFabricNodeRole(role.role)).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 webIngressReceiverCluster = webIngressReceiverClusterSummary(nodes, heartbeatsByNode); 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 && ( <>