Files
rdp-proxy/web-admin/src/App.tsx
T
2026-05-14 23:30:34 +03:00

9840 lines
495 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { 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<string, string> = {
"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<string, string[]> = {
"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, 124 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<Language, Record<string, string>>;
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<ConsoleMode | null> {
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<WebAdminSession | null>(() => loadStoredSession());
const [consoleMode, setConsoleMode] = useState<ConsoleMode | null>(null);
const [sessionRefreshedAt, setSessionRefreshedAt] = useState("");
const [language, setLanguage] = useState<Language>(() => (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<InstallationStatus | null>(null);
const [bootstrapForm, setBootstrapForm] = useState({
email: "",
password: "",
activationPayload: "",
activationSignature: "",
});
const [activeView, setActiveView] = useState<ViewId>("command");
const [selectedClusterId, setSelectedClusterId] = useState("");
const [clusters, setClusters] = useState<Cluster[]>([]);
const [clusterSummaries, setClusterSummaries] = useState<ClusterAdminSummary[]>([]);
const [authority, setAuthority] = useState<ClusterAuthorityState | null>(null);
const [nodes, setNodes] = useState<ClusterNode[]>([]);
const [nodeGroups, setNodeGroups] = useState<ClusterNodeGroup[]>([]);
const [allNodesByCluster, setAllNodesByCluster] = useState<Record<string, ClusterNode[]>>({});
const [joinRequests, setJoinRequests] = useState<JoinRequest[]>([]);
const [joinTokens, setJoinTokens] = useState<NodeJoinToken[]>([]);
const [releaseVersions, setReleaseVersions] = useState<ReleaseVersion[]>([]);
const [nodeUpdatePlansByNode, setNodeUpdatePlansByNode] = useState<Record<string, NodeUpdatePlan>>({});
const [nodeUpdateStatusesByNode, setNodeUpdateStatusesByNode] = useState<Record<string, NodeUpdateStatus[]>>({});
const [rolesByNode, setRolesByNode] = useState<Record<string, RoleAssignment[]>>({});
const [desiredWorkloadsByNode, setDesiredWorkloadsByNode] = useState<Record<string, NodeWorkloadDesiredState[]>>({});
const [workloadsByNode, setWorkloadsByNode] = useState<Record<string, WorkloadStatus[]>>({});
const [heartbeatsByNode, setHeartbeatsByNode] = useState<Record<string, NodeHeartbeat[]>>({});
const [telemetryByNode, setTelemetryByNode] = useState<Record<string, NodeTelemetryObservation[]>>({});
const [meshLinks, setMeshLinks] = useState<MeshLink[]>([]);
const [routeIntents, setRouteIntents] = useState<MeshRouteIntent[]>([]);
const [syntheticMeshConfigsByNode, setSyntheticMeshConfigsByNode] = useState<Record<string, NodeSyntheticMeshConfig>>({});
const [fabricRouteFeedback, setFabricRouteFeedback] = useState<FabricServiceChannelRouteFeedbackObservation[]>([]);
const [fabricRebuildAttempts, setFabricRebuildAttempts] = useState<FabricServiceChannelRouteRebuildAttempt[]>([]);
const [fabricRebuildHealth, setFabricRebuildHealth] = useState<FabricServiceChannelRouteRebuildHealthSummary | null>(null);
const [fabricRebuildSilences, setFabricRebuildSilences] = useState<FabricServiceChannelRouteRebuildAlertSilence[]>([]);
const [fabricReadiness, setFabricReadiness] = useState<FabricServiceChannelReadiness | null>(null);
const [fabricSchemaStatus, setFabricSchemaStatus] = useState<FabricServiceChannelSchemaStatus | null>(null);
const [fabricSnapshotHealth, setFabricSnapshotHealth] = useState<FabricServiceChannelRebuildSnapshotMaintenanceHealth | null>(null);
const [fabricSnapshotWarmup, setFabricSnapshotWarmup] = useState<FabricServiceChannelRebuildSnapshotWarmup | null>(null);
const [fabricLeaseMaintenance, setFabricLeaseMaintenance] = useState<FabricServiceChannelLeaseMaintenance | null>(null);
const [fabricAccessTelemetry, setFabricAccessTelemetry] = useState<FabricServiceChannelAccessTelemetry | null>(null);
const [fabricRebuildIncidents, setFabricRebuildIncidents] = useState<FabricServiceChannelRouteRebuildIncident[]>([]);
const [fabricRebuildLedgerDeep, setFabricRebuildLedgerDeep] = useState(false);
const [fabricRebuildLedgerFilters, setFabricRebuildLedgerFilters] = useState<FabricRebuildLedgerFilters>(defaultFabricRebuildLedgerFilters);
const [fabricRecoveryPolicy, setFabricRecoveryPolicy] = useState<FabricServiceChannelRecoveryPolicy | null>(null);
const [fabricBreadcrumbWindowPolicy, setFabricBreadcrumbWindowPolicy] = useState<FabricServiceChannelBreadcrumbWindowPolicy | null>(null);
const [qosPolicies, setQosPolicies] = useState<QoSPolicy[]>([]);
const [entryPoints, setEntryPoints] = useState<FabricEntryPoint[]>([]);
const [entryPointNodesById, setEntryPointNodesById] = useState<Record<string, FabricEntryPointNode[]>>({});
const [egressPools, setEgressPools] = useState<FabricEgressPool[]>([]);
const [egressPoolNodesById, setEgressPoolNodesById] = useState<Record<string, FabricEgressPoolNode[]>>({});
const [testingFlags, setTestingFlags] = useState<FabricTestingFlag[]>([]);
const [vpnConnections, setVPNConnections] = useState<VPNConnection[]>([]);
const [vpnLeases, setVPNLeases] = useState<Record<string, VPNConnectionLease | null>>({});
const [vpnPacketStats, setVPNPacketStats] = useState<Record<string, VPNPacketStats>>({});
const [vpnDiagnosticDeviceId, setVPNDiagnosticDeviceId] = useState(() => localStorage.getItem(storageKeys.vpnDiagnosticDeviceId) || "");
const [vpnClientDiagnostics, setVPNClientDiagnostics] = useState<VPNClientDiagnosticStatus[]>([]);
const [vpnClientDiagnostic, setVPNClientDiagnostic] = useState<VPNClientDiagnosticStatus | null>(null);
const [vpnDiagnosticTestUrl, setVPNDiagnosticTestUrl] = useState("http://2ip.ru/");
const [lastVPNDiagnosticCommand, setLastVPNDiagnosticCommand] = useState<VPNClientDiagnosticCommand | null>(null);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [users, setUsers] = useState<UserAccount[]>([]);
const [resources, setResources] = useState<Resource[]>([]);
const [membershipsByOrg, setMembershipsByOrg] = useState<Record<string, OrganizationMembership[]>>({});
const [audit, setAudit] = useState<AuditEvent[]>([]);
const [fabricDrilldownAudit, setFabricDrilldownAudit] = useState<AuditEvent[]>([]);
const [fabricDrilldownAuditSummary, setFabricDrilldownAuditSummary] = useState<AuditSummary | null>(null);
const [lastDataRefreshAt, setLastDataRefreshAt] = useState("");
const [liveTransport, setLiveTransport] = useState<"sse" | "poll">("poll");
const [organizationId, setOrganizationId] = useState("");
const [organizationSummary, setOrganizationSummary] = useState<OrganizationAdminSummary | null>(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<JoinTokenFormState>(defaultJoinTokenForm);
const [lastJoinToken, setLastJoinToken] = useState<CreatedJoinToken | null>(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<string[]>([]);
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<string[]>([]);
const [nodeInfoDialog, setNodeInfoDialog] = useState<NodeInventoryEntry | null>(null);
const [nodeInfoMode, setNodeInfoMode] = useState<"details" | "manage">("details");
const [nodeRoleDrafts, setNodeRoleDrafts] = useState<Record<string, string>>({});
const [nodeWorkloadDrafts, setNodeWorkloadDrafts] = useState<Record<string, string>>({});
const [meshListenerDrafts, setMeshListenerDrafts] = useState<
Record<string, { listenAddr: string; mode: string; autoRange: string; advertiseEndpoint: string; advertiseTransport: string; connectivity: string; nat: string; region: string }>
>({});
const [entryPointNodeDrafts, setEntryPointNodeDrafts] = useState<Record<string, string>>({});
const [egressPoolNodeDrafts, setEgressPoolNodeDrafts] = useState<Record<string, string>>({});
const [nodeTestingDrafts, setNodeTestingDrafts] = useState<Record<string, { telemetry: boolean; links: boolean }>>({});
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<Resource | null>(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<unknown>, 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<string, unknown>, 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 (
<main className="loginShell">
{installationStatus && (
<section className="loginCard">
<h1>{installationStatus.bootstrapped ? t.installationLocked : t.bootstrapTitle}</h1>
<StateLine label="Authority" value={`${installationStatus.authority_mode}/${installationStatus.authority_state}`} />
<StateLine label="Strict" value={installationStatus.strict_authority ? "enabled" : "legacy"} />
{installationStatus.root_fingerprint && <StateLine label="Root key" value={shortId(installationStatus.root_fingerprint)} />}
</section>
)}
{bootstrapRequired ? (
<section className="loginCard">
<h1>{t.bootstrapTitle}</h1>
<p className="loginHint">{bootstrapBlocked ? t.insecureBootstrapDisabled : t.bootstrapText}</p>
<label>
{t.email}
<input
value={bootstrapForm.email}
onChange={(event) => setBootstrapForm({ ...bootstrapForm, email: event.target.value })}
autoComplete="username"
/>
</label>
<label>
{t.password}
<input
value={bootstrapForm.password}
onChange={(event) => setBootstrapForm({ ...bootstrapForm, password: event.target.value })}
type="password"
autoComplete="new-password"
/>
</label>
{installationStatus?.strict_authority && (
<>
<label>
{t.activationPayload}
<textarea
value={bootstrapForm.activationPayload}
onChange={(event) => setBootstrapForm({ ...bootstrapForm, activationPayload: event.target.value })}
spellCheck={false}
/>
</label>
<label>
{t.activationSignature}
<input
value={bootstrapForm.activationSignature}
onChange={(event) => setBootstrapForm({ ...bootstrapForm, activationSignature: event.target.value })}
spellCheck={false}
/>
</label>
</>
)}
{error && <div className="errorPanel">{error}</div>}
{notice && <div className="noticePanel">{notice}</div>}
<button
className="primary wide"
onClick={() => void handleBootstrapOwner()}
disabled={
loading ||
bootstrapBlocked ||
!bootstrapForm.email ||
bootstrapForm.password.length < 12 ||
(installationStatus?.strict_authority && (!bootstrapForm.activationPayload || !bootstrapForm.activationSignature))
}
>
{loading ? t.creatingOwner : t.createOwner}
</button>
</section>
) : (
<section className="loginCard">
<h1>{t.signInTitle}</h1>
<label>
{t.email}
<input
value={loginForm.email}
onChange={(event) => setLoginForm({ ...loginForm, email: event.target.value.trim() })}
autoComplete="username"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
/>
</label>
<label>
{t.password}
<input
value={loginForm.password}
onChange={(event) => setLoginForm({ ...loginForm, password: event.target.value })}
type={loginForm.showPassword ? "text" : "password"}
autoComplete="current-password"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
onKeyDown={(event) => {
if (event.key === "Enter") {
void handleLogin();
}
}}
/>
</label>
<label className="checkLine">
<input
type="checkbox"
checked={loginForm.showPassword}
onChange={(event) => setLoginForm({ ...loginForm, showPassword: event.target.checked })}
/>
Показать пароль
</label>
<label className="checkLine">
<input
type="checkbox"
checked={loginForm.trustDevice}
onChange={(event) => setLoginForm({ ...loginForm, trustDevice: event.target.checked })}
/>
{t.trustDevice}
</label>
<label className="checkLine">
<input
type="checkbox"
checked={loginForm.rememberMe}
onChange={(event) => setLoginForm({ ...loginForm, rememberMe: event.target.checked })}
/>
{t.rememberMe}
</label>
{error && <div className="errorPanel">{error}</div>}
{notice && <div className="noticePanel">{notice}</div>}
<button className="primary wide" onClick={() => void handleLogin()} disabled={loading || !loginForm.email || !loginForm.password}>
{loading ? t.signingIn : t.signIn}
</button>
</section>
)}
</main>
);
}
if (session && !consoleMode) {
return (
<main className="loginShell">
<section className="loginCard">
<p>{loading ? t.lastRefresh : "Восстанавливаем сессию..."}</p>
</section>
</main>
);
}
if (consoleMode === "user") {
const activeOrganization = organizations.find((org) => org.id === organizationId) || organizations[0] || null;
const visibleResources = activeOrganization ? resources.filter((resource) => resource.organization_id === activeOrganization.id) : resources;
const activeMembership = activeOrganization
? (membershipsByOrg[activeOrganization.id] || []).find((membership) => membership.user_id === session.userId)
: null;
const protocolCounts = visibleResources.reduce<Record<string, number>>((acc, resource) => {
acc[resource.protocol] = (acc[resource.protocol] || 0) + 1;
return acc;
}, {});
return (
<main className="portalShell">
<aside className="portalRail">
<div className="brandMark">RAP</div>
<p className="sideKicker">Личный кабинет</p>
<h1>Мой доступ</h1>
<p className="sideText">Установки, доступные серверы и состояние рабочей области пользователя.</p>
<StateLine label={t.sessionMode} value={`${sessionModeLabel}${sessionRefreshedAt ? formatTime(sessionRefreshedAt) : "н/д"}`} />
<StateLine label={t.actorUser} value={session.email} />
<button className="ghost" onClick={() => void handleLogout()} disabled={loading}>
{t.logout}
</button>
</aside>
<section className="portalWorkspace">
<header className="portalTop">
<div>
<p className="eyebrow">Secure Access Fabric</p>
<h2>{activeOrganization?.name || "Личный кабинет"}</h2>
<p className="muted">{session.email}</p>
</div>
<label>
Организация
<select value={activeOrganization?.id || ""} onChange={(event) => setOrganizationId(event.target.value)}>
{organizations.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
</label>
<button className="primary" onClick={() => void refreshUserPortal()} disabled={loading}>
{loading ? t.refreshing : t.refresh}
</button>
</header>
{error && <div className="errorPanel">{error}</div>}
{notice && <div className="noticePanel">{notice}</div>}
<section className="grid three">
<Metric label="Организации" value={organizations.length} tone="steel" />
<Metric label="Серверы" value={visibleResources.length} tone="green" />
<Metric label="Установки" value={2} tone="amber" />
</section>
<section className="grid two">
<article className="card">
<div className="cardHead">
<div>
<h3>Установки</h3>
<p className="muted">
{androidClientVersion ? `Актуальная версия Android: ${androidClientVersion}` : "Скачивайте актуальные клиенты только отсюда, чтобы не ловить старую сборку."}
</p>
</div>
<span className="status active">latest</span>
</div>
<div className="portalInstallList">
<a className="installTile primaryInstall" href={androidVPNDownloadUrl}>
<strong>Android VPN</strong>
<span>Последняя сборка RAP HOME VPN для телефона</span>
<small>{androidClientVersionedPath || portalAndroidArtifactPath}</small>
</a>
<a className="installTile" href={`${portalDownloadBaseUrl}/downloads/rap-windows-rdp-client-latest-win-x64.zip`}>
<strong>Windows RDP клиент</strong>
<span>Клиент удаленного рабочего стола, когда нужен доступ к серверам</span>
<small>latest win-x64</small>
</a>
</div>
</article>
<article className="card">
<h3>Профиль</h3>
<StateLine label="Пользователь" value={session.email} />
<StateLine label="Роль в организации" value={activeMembership?.role_id || "участник"} />
<StateLine label="Организация" value={activeOrganization?.name || "нет"} />
<StateLine label="Последнее обновление" value={lastDataRefreshAt ? formatDate(lastDataRefreshAt) : "нет"} />
</article>
<article className="card span2">
<div className="cardHead">
<div>
<h3>Доступные серверы</h3>
<p className="muted">Список ресурсов, которые уже разрешены пользователю через организацию.</p>
</div>
</div>
<DataTable
columns={["имя", "адрес", "протокол", "секрет", "передача файлов"]}
rows={visibleResources.map((resource) => [
resource.name,
resource.address,
resource.protocol,
resource.has_secret ? "настроен" : "нет",
statusLabel(resource.file_transfer_mode || "disabled"),
])}
/>
</article>
<article className="card">
<h3>Сервисы</h3>
<DataTable columns={["тип", "количество"]} rows={Object.entries(protocolCounts).map(([protocol, count]) => [protocol, String(count)])} />
</article>
<article className="card">
<h3>Что здесь будет дальше</h3>
<div className="portalRoadmap">
<span>Устройства и доверенные входы</span>
<span>Активные VPN/RDP сессии</span>
<span>Обновление профиля VPN без ручных ключей</span>
<span>Самостоятельная смена пароля</span>
</div>
</article>
</section>
</section>
</main>
);
}
return (
<main className="consoleShell">
<aside className="sideRail">
<div className="brandMark">SAF</div>
<p className="sideKicker">{t.productOwner}</p>
<h1>{t.controlPlane}</h1>
<p className="sideText">{t.sideText}</p>
<nav className="railNav">
{views.filter((view) => view.id !== "roles").map((view) => (
<button key={view.id} className={activeView === view.id ? "active" : ""} onClick={() => setActiveView(view.id)}>
{view[language]}
</button>
))}
</nav>
</aside>
<section className="workspace">
<header className="topBar">
<div>
<p className="eyebrow">Secure Access Fabric</p>
<h2>{selectedCluster ? selectedCluster.name : t.consoleTitle}</h2>
<p className="muted">{t.boundary}</p>
</div>
<div className="clusterPicker">
<label>
{t.activeCluster}
<select value={selectedClusterId} onChange={(event) => void switchCluster(event.target.value)}>
{clusters.map((cluster) => (
<option key={cluster.id} value={cluster.id}>
{cluster.name}
</option>
))}
</select>
</label>
<span>
{t.slugLabel}: {selectedCluster?.slug || "н/д"}
</span>
</div>
<button className="primary" onClick={() => void refreshAll()} disabled={loading}>
{loading ? t.refreshing : t.refresh}
</button>
<div className="refreshStatus">
<strong>{t.autoRefresh}</strong>
<span>{lastDataRefreshAt ? `${t.lastRefresh}: ${formatTime(lastDataRefreshAt)} / ${liveTransport.toUpperCase()}` : liveTransport.toUpperCase()}</span>
</div>
<div className="profilePanel">
<strong>{t.profile}</strong>
<span>{session.email}</span>
<span>
{t.sessionMode}: {sessionModeLabel} | {t.sessionRefreshedAt}: {sessionRefreshedAt ? formatTime(sessionRefreshedAt) : "н/д"}
</span>
<label>
{t.language}
<select value={language} onChange={(event) => setLanguage(event.target.value as Language)}>
<option value="ru">Русский</option>
<option value="en">{language === "ru" ? "Английский" : "English"}</option>
</select>
</label>
<button className="ghost" onClick={() => void handleLogout()} disabled={loading}>
{t.logout}
</button>
</div>
</header>
{error && <div className="errorPanel">{error}</div>}
{notice && <div className="noticePanel">{notice}</div>}
{selectedCluster && nodes.length === 0 && (
<div className="noticePanel">
<strong>{t.emptyLiveTitle}.</strong> {t.emptyLiveText}
</div>
)}
{activeView === "command" && (
<section className="grid five">
<Metric label="Кластеры" value={clusters.length} tone="steel" />
<Metric label="Узлы в области" value={nodes.length} tone="green" />
<Metric label="Здоровые узлы" value={healthyNodeCount} tone="green" />
<Metric label="Ожидают подключения" value={pendingJoinCount} tone="amber" />
<Metric label="Рискованные состояния" value={riskyNodeCount} tone="red" />
<article className="card span3">
<h3>Общее состояние кластеров</h3>
<DataTable
columns={["кластер", "authority", "ключ", "режим изменений", "узлы", "заявки", "роли", "последний сигнал"]}
rows={clusterSummaries.map((summary) => [
summary.name,
summary.authority_state,
shortId(summary.cluster_key_fingerprint),
summary.mutation_mode,
`${summary.healthy_node_count}/${summary.node_count}`,
String(summary.pending_join_count),
String(summary.active_role_assignment_count),
formatDate(summary.last_node_seen_at),
])}
/>
</article>
<article className="card span2">
<h3>Authority выбранного кластера</h3>
{authority ? (
<div className="stateList">
<StateLine label="Authority" value={authority.authority_state} />
<StateLine label="Режим изменений" value={authority.mutation_mode} />
<StateLine label="Терм" value={String(authority.term)} />
<StateLine label="Cluster key" value={shortId(selectedSummary?.cluster_key_fingerprint)} />
<StateLine label="Обновлено" value={formatDate(authority.updated_at)} />
</div>
) : (
<EmptyState title="Нет состояния authority" text="Выберите кластер, чтобы загрузить состояние authority." />
)}
</article>
<article className="card span2">
<h3>Граница платформы</h3>
<p className="muted">
Эта панель предназначена для владельца продукта / владельца платформы. Панели организаций должны использовать безопасные
проекции и не раскрывать mesh internals, peer cache, route cache, секреты или данные других tenants.
</p>
</article>
<article className="card span3">
<h3>Текущие сигналы кластера</h3>
<div className="signalStrip">
<Signal label="Активные роли" value={String(activeRoleCount)} />
<Signal label="Отчеты сервисов" value={String(Object.values(workloadsByNode).filter((items) => items.length > 0).length)} />
<Signal label="Наблюдения связей" value={String(meshLinks.length)} />
<Signal label="Synthetic configs" value={`${syntheticConfigEnabledCount}/${nodes.length}`} />
</div>
</article>
</section>
)}
{activeView === "clusters" && (
<section className="grid two">
<article className="card span2">
<div className="cardHead">
<div>
<h3>{t.clusterCatalog}</h3>
<p className="muted">{t.clusterCatalogText}</p>
</div>
<span className="pill">{clusterCountLabel(clusters.length, language)}</span>
</div>
<div className="clusterCatalog">
{clusters.map((cluster) => {
const summary = clusterSummaries.find((item) => item.cluster_id === cluster.id);
const isSelected = cluster.id === selectedClusterId;
return (
<article key={cluster.id} className={`clusterCard ${isSelected ? "selected" : ""}`}>
<div className="clusterCardMain">
<div>
<p className="eyebrow">{cluster.region || "регион не задан"}</p>
<h4>{cluster.name}</h4>
<p className="muted">
{t.slugLabel}: <strong>{cluster.slug}</strong>
</p>
</div>
<div className="clusterCardActions">
<Status value={cluster.status} />
{isSelected ? (
<span className="pill good">{t.selected}</span>
) : (
<button onClick={() => void switchCluster(cluster.id)}>{t.makeActive}</button>
)}
<button
className="ghost"
onClick={() => {
void switchCluster(cluster.id);
setActiveView("cluster-settings");
}}
>
{t.openSettings}
</button>
</div>
</div>
<div className="signalStrip compact">
<Signal label="Узлы" value={summary ? `${summary.healthy_node_count}/${summary.node_count}` : "н/д"} />
<Signal label="Заявки" value={String(summary?.pending_join_count ?? "н/д")} />
<Signal label="Роли" value={String(summary?.active_role_assignment_count ?? "н/д")} />
<Signal label="Последний сигнал" value={formatDate(summary?.last_node_seen_at)} />
</div>
<details>
<summary>{t.clusterDetails}</summary>
<div className="stateList">
<StateLine label="ID" value={cluster.id} />
<StateLine label={t.slugLabel} value={cluster.slug} />
<StateLine label="Статус" value={statusLabel(cluster.status)} />
<StateLine label="Authority" value={summary ? `${summary.authority_state}/${summary.mutation_mode}` : "неизвестно"} />
<StateLine label="Создан" value={formatDate(cluster.created_at)} />
<StateLine label="Обновлен" value={formatDate(cluster.updated_at || cluster.created_at)} />
</div>
</details>
</article>
);
})}
{clusters.length === 0 && <EmptyState title="Кластеров нет" text="Создайте первый кластер, затем подключите стартовый node-agent." />}
</div>
</article>
<article className="card">
<h3>{t.createCluster}</h3>
<FormGrid>
<label>
{t.slugLabel}
<input value={clusterForm.slug} onChange={(event) => setClusterForm({ ...clusterForm, slug: event.target.value })} />
</label>
<label>
Название
<input value={clusterForm.name} onChange={(event) => setClusterForm({ ...clusterForm, name: event.target.value })} />
</label>
<label>
Регион
<input value={clusterForm.region} onChange={(event) => setClusterForm({ ...clusterForm, region: event.target.value })} />
</label>
</FormGrid>
<p className="muted">{t.slugHelp}</p>
<button
className="primary"
disabled={!clusterForm.slug || !clusterForm.name}
onClick={() =>
void runAction(async () => {
await client.createCluster({ slug: clusterForm.slug, name: clusterForm.name, region: clusterForm.region || null });
setClusterForm({ slug: "", name: "", region: "" });
}, "Кластер создан.")
}
>
{t.createCluster}
</button>
</article>
<article className="card">
<h3>Что такое технический код?</h3>
<p className="muted">{t.slugHelp}</p>
<p className="muted">
Для человека основное поле название. Для системы и операторов технический код. Он нужен, чтобы сценарии, логи и будущие
endpoint-адреса не зависели от переименования кластера.
</p>
</article>
</section>
)}
{activeView === "cluster-settings" && (
<section className="grid two">
{!selectedCluster && <EmptyState title="Кластер не выбран" text="Выберите активный кластер, чтобы открыть настройки." />}
{selectedCluster && (
<>
<article className="card">
<h3>Identity кластера</h3>
<p className="muted">Базовые параметры хранятся в PostgreSQL. Slug остается неизменяемым идентификатором для операторов и скриптов.</p>
<FormGrid>
<label>
ID
<input value={selectedCluster.id} readOnly />
</label>
<label>
Slug
<input value={selectedCluster.slug} readOnly />
</label>
<label>
Название
<input
value={clusterSettingsForm.name}
onChange={(event) => setClusterSettingsForm({ ...clusterSettingsForm, name: event.target.value })}
/>
</label>
<label>
Статус
<select
value={clusterSettingsForm.status}
onChange={(event) => setClusterSettingsForm({ ...clusterSettingsForm, status: event.target.value })}
>
<option value="active">active, работает</option>
<option value="disabled">disabled, отключен</option>
</select>
</label>
<label>
Регион
<input
value={clusterSettingsForm.region}
onChange={(event) => setClusterSettingsForm({ ...clusterSettingsForm, region: event.target.value })}
placeholder="например ru-msk-1"
/>
</label>
<label>
Обновлен
<input value={formatDate(selectedCluster.updated_at || selectedCluster.created_at)} readOnly />
</label>
</FormGrid>
<label className="wideLabel">
Metadata JSON
<textarea
value={clusterSettingsForm.metadataJson}
onChange={(event) => setClusterSettingsForm({ ...clusterSettingsForm, metadataJson: event.target.value })}
rows={8}
spellCheck={false}
/>
</label>
<button
className="primary"
disabled={!clusterSettingsForm.name.trim()}
onClick={() =>
confirmHighRisk("Сохранить базовые настройки кластера") &&
void runAction(async () => {
const metadata = parseJSONObject(clusterSettingsForm.metadataJson || "{}", "Metadata JSON");
await client.updateCluster(selectedCluster.id, {
name: clusterSettingsForm.name,
status: clusterSettingsForm.status,
region: clusterSettingsForm.region || null,
metadata,
});
}, "Настройки кластера сохранены.")
}
>
Сохранить настройки кластера
</button>
</article>
<article className="card">
<h3>Authority и режим изменений</h3>
<p className="muted">Эта секция защищает кластер от split-brain: minority/read-only сегменты не должны принимать изменения политик.</p>
<div className="stateGrid">
<StateLine label="Authority" value={authority?.authority_state || "неизвестно"} />
<StateLine label="Mutation mode" value={authority?.mutation_mode || "неизвестно"} />
<StateLine label="Term" value={String(authority?.term ?? "н/д")} />
<StateLine label="Cluster key" value={shortId(selectedSummary?.cluster_key_fingerprint)} />
<StateLine label="Последнее изменение" value={formatDate(authority?.updated_at)} />
</div>
<FormGrid>
<label>
Состояние authority
<select
value={authorityForm.authorityState}
onChange={(event) => setAuthorityForm({ ...authorityForm, authorityState: event.target.value })}
>
<option value="authoritative">authoritative, основной</option>
<option value="minority">minority, меньшинство</option>
<option value="isolated">isolated, изолирован</option>
<option value="recovery">recovery, восстановление</option>
</select>
</label>
<label>
Режим изменений
<select value={authorityForm.mutationMode} onChange={(event) => setAuthorityForm({ ...authorityForm, mutationMode: event.target.value })}>
<option value="normal">normal, обычный</option>
<option value="read_only">read_only, только чтение</option>
<option value="recovery_override">recovery_override, восстановление</option>
</select>
</label>
<label>
Примечание
<input value={authorityForm.notes} onChange={(event) => setAuthorityForm({ ...authorityForm, notes: event.target.value })} />
</label>
</FormGrid>
<button
disabled={!selectedClusterId}
onClick={() =>
confirmHighRisk("Изменить authority state кластера") &&
void runAction(
() =>
client.updateClusterAuthority(selectedClusterId, {
authorityState: authorityForm.authorityState,
mutationMode: authorityForm.mutationMode,
notes: authorityForm.notes,
}),
"Authority кластера обновлен.",
)
}
>
Обновить authority
</button>
</article>
<article className="card">
<h3>Safety / quorum</h3>
<div className="stateGrid">
<StateLine label="Узлы" value={String(selectedSummary?.node_count ?? nodes.length)} />
<StateLine label="Healthy" value={String(selectedSummary?.healthy_node_count ?? healthyNodeCount)} />
<StateLine label="Pending join" value={String(selectedSummary?.pending_join_count ?? joinRequests.filter((item) => item.status === "pending").length)} />
<StateLine label="Последний узел" value={formatDate(selectedSummary?.last_node_seen_at)} />
</div>
<p className="muted">
Минимальный размер, quorum policy и split-brain rules пока не имеют отдельного runtime-переключателя. Сейчас защита выполняется через
authority/mutation mode, explicit node approval и аудит.
</p>
</article>
<article className="card">
<h3>Telemetry / testing</h3>
<div className="stateGrid">
<StateLine label="Telemetry flag" value={platformTestingFlag?.telemetry_enabled ? "включен" : "выключен"} />
<StateLine label="Synthetic links" value={platformTestingFlag?.synthetic_links_enabled ? "включены" : "выключены"} />
<StateLine label="Хранение истории, часов" value={String(platformTestingFlag?.history_retention_hours ?? "н/д")} />
</div>
<p className="muted">
Это тестовый контур наблюдаемости: heartbeat/telemetry реальные, а связи Fabric сейчас synthetic. Production mesh traffic здесь пока не
отображается.
</p>
</article>
<article className="card">
<h3>Storage / updates</h3>
<div className="stateGrid">
<StateLine label="Version Storage" value="архитектура зафиксирована, runtime не реализован" />
<StateLine label="Update cache" value={`${nodesWithRole("update-cache", rolesByNode).length} узл.`} />
<StateLine label="File/config cache" value={`${nodesWithRole("file-storage-cache", rolesByNode).length} узл.`} />
</div>
<p className="muted">
Version Storage будет хранить stable/current/candidate и signed artifacts. Сейчас это не production updater runtime.
</p>
</article>
<article className="card">
<h3>Admin endpoints</h3>
<div className="stateGrid">
<StateLine label="Entry nodes" value={`${nodesWithRole("entry-node", rolesByNode).length} узл.`} />
<StateLine label="Relay nodes" value={`${nodesWithRole("relay-node", rolesByNode).length} узл.`} />
<StateLine label="Core mesh" value={`${nodesWithRole("core-mesh", rolesByNode).length} узл.`} />
</div>
<p className="muted">
Панель кластера не переезжает автоматически на storage-узел. Cluster Admin Endpoint должен быть назначен отдельной explicit ролью на
ingress/admin-capable узле.
</p>
</article>
</>
)}
</section>
)}
{activeView === "nodes" && (
<section className="grid two">
<article className="card span2">
<div className="cardHead">
<div>
<h3>{t.nodeManagement}</h3>
<p className="muted">
Единый краткий список узлов. По умолчанию показан активный кластер; включите общий режим, чтобы увидеть весь инвентарь платформы.
</p>
</div>
<div className="actions">
<label className="checkLine">
<input
type="checkbox"
checked={nodeViewScope === "all"}
onChange={(event) => setNodeViewScope(event.target.checked ? "all" : "cluster")}
/>
{t.showAllPlatformNodes}
</label>
<button
className="ghost"
onClick={() => {
setNodeViewScope("all");
setAllNodeSearch("");
}}
>
{t.showAllPlatformNodes}
</button>
</div>
</div>
<div className="signalStrip compact">
<Signal label="Узлы активного кластера" value={String(nodes.length)} />
<Signal label="Все узлы" value={String(allNodeInventory.length)} />
<Signal label="Заявки" value={String(pendingJoinCount)} />
<Signal label="Активные роли" value={String(activeRoleCount)} />
</div>
<p className="muted">{t.addNodeText}</p>
</article>
<article className="card span2">
<div className="cardHead">
<div>
<h3>{t.nodeBriefList}</h3>
<p className="muted">{t.nodeBriefListHelp}</p>
</div>
<span className="pill">{visibleNodeInventory.length}</span>
</div>
<FormGrid>
<label>
{t.nodeSearch}
<input value={allNodeSearch} onChange={(event) => setAllNodeSearch(event.target.value)} placeholder={t.nodeSearchPlaceholder} />
</label>
<label>
{t.nodeGroupFilter}
<select value={nodeGroupFilterId} onChange={(event) => setNodeGroupFilterId(event.target.value)}>
<option value="">{t.allNodeGroups}</option>
{nodeGroups.map((group) => (
<option key={group.id} value={group.id}>
{groupPathLabel(group, nodeGroups)}
</option>
))}
</select>
</label>
</FormGrid>
<p className="muted">{t.nodeGroupInventoryText}</p>
<h4>{t.nodeGroupCreatePanel}</h4>
<FormGrid>
<label>
{t.nodeGroupName}
<input value={nodeGroupForm.name} onChange={(event) => setNodeGroupForm({ ...nodeGroupForm, name: event.target.value })} />
</label>
<label>
{t.parentNodeGroup}
<select value={nodeGroupForm.parentGroupId} onChange={(event) => setNodeGroupForm({ ...nodeGroupForm, parentGroupId: event.target.value })}>
<option value="">{t.rootNodeGroup}</option>
{nodeGroups.map((group) => (
<option key={group.id} value={group.id}>
{groupPathLabel(group, nodeGroups)}
</option>
))}
</select>
</label>
<label>
{t.createNodeGroup}
<button
className="primary"
disabled={!nodeGroupForm.name.trim()}
onClick={() =>
void runAction(
async () => {
await client.createNodeGroup(selectedClusterId, {
name: nodeGroupForm.name,
parentGroupId: nodeGroupForm.parentGroupId || null,
});
setNodeGroupForm({ name: "", parentGroupId: "" });
},
t.nodeGroupCreated,
)
}
>
{t.createNodeGroup}
</button>
</label>
</FormGrid>
<div className="nodeList">
{visibleNodeTreeRows.map((row) => {
if (row.kind === "group") {
const isCollapsed = collapsedNodeGroupKeys.includes(row.key);
return (
<div className="nodeListGroup" key={row.key} style={{ paddingLeft: `${row.depth * 18}px` }}>
<div className="nodeListMain">
<strong>{row.label}</strong>
{row.groupId && <span>{groupPathLabelById(row.groupId, nodeGroups)}</span>}
</div>
<div className="actions">
<span className="pill">{row.count}</span>
<button className="ghost" onClick={() => setCollapsedNodeGroupKeys(toggleArrayValue(collapsedNodeGroupKeys, row.key))}>
{isCollapsed ? t.expandGroup : t.collapseGroup}
</button>
{row.groupId && (
<button className="ghost" onClick={() => setNodeGroupForm({ name: "", parentGroupId: row.groupId || "" })}>
{t.createSubgroup}
</button>
)}
</div>
</div>
);
}
const entry = row.entry;
const activeMembership = entry.memberships.find((membership) => membership.cluster.id === selectedClusterId);
const displayNode = activeMembership?.node || entry.node;
const runtime = nodeRuntimeConnectivity(displayNode, heartbeatsByNode[displayNode.id] || [], meshLinks);
const update = nodeUpdateSummary(displayNode, nodeUpdatePlansByNode[displayNode.id], releaseVersions);
const updater = nodeUpdaterSummary(nodeUpdateStatusesByNode[displayNode.id] || []);
const isActiveMember = activeMembership?.node.membership_status === "active";
const isRevokedMember = activeMembership?.node.membership_status === "revoked";
return (
<div className="nodeListRow" key={row.key} style={{ marginLeft: `${row.depth * 18}px` }}>
<div className="nodeListMain">
<strong>{displayNode.name}</strong>
<span>{displayNode.node_key}</span>
<small className="muted">{runtime.address}</small>
</div>
<Status value={displayNode.health_status} />
<RuntimeBadges runtime={runtime} />
<div className="nodeEndpointCell">
<strong>{displayNode.reported_version || "версия неизвестна"}</strong>
<small>{update.targetLabel}</small>
</div>
<Status value={update.status} />
<div className="nodeEndpointCell">
<strong className={`pill ${updater.tone}`}>{updater.label}</strong>
<small>{updater.detail}</small>
</div>
<span className="muted">{formatDate(displayNode.last_seen_at)}</span>
{activeMembership ? <Status value={activeMembership.node.membership_status} /> : <span className="muted">{t.notMemberOfActiveCluster}</span>}
<div className="actions">
<button
onClick={() => {
setNodeInfoDialog(entry);
setNodeInfoMode("details");
}}
>
{t.nodeDetails}
</button>
{isActiveMember ? (
<>
<button
className="primary"
onClick={() => {
setNodeInfoDialog(entry);
setNodeInfoMode("manage");
}}
>
{t.manageNode}
</button>
<button
className="danger"
onClick={() =>
confirmHighRisk(`Удалить узел ${displayNode.name} из кластера`) &&
void runAction(
() => client.deleteClusterNode(selectedClusterId, displayNode.id, "Удалено из списка узлов панели владельца платформы."),
"Узел удален из кластера.",
)
}
>
Удалить
</button>
</>
) : isRevokedMember ? (
<span className="muted">{t.revokedMembership}</span>
) : (
<button
className="primary"
onClick={() => {
setAttachNodeDialog(entry);
setAttachNodeRoles([]);
}}
>
{t.connectExistingNode}
</button>
)}
</div>
</div>
);
})}
{visibleNodeTreeRows.length === 0 && <EmptyState title={t.noNodesTitle} text={t.noNodesByFilter} />}
</div>
</article>
{attachNodeDialog && (
<div className="modalBackdrop" role="presentation">
<div className="modalCard" role="dialog" aria-modal="true" aria-labelledby="attach-node-title">
<div className="cardHead">
<div>
<h3 id="attach-node-title">{t.connectExistingNodeTitle}</h3>
<p className="muted">{t.connectExistingNodeText}</p>
</div>
<button className="ghost" onClick={() => setAttachNodeDialog(null)}>
{t.cancel}
</button>
</div>
<div className="stateList">
<StateLine label="Узел" value={attachNodeDialog.node.name} />
<StateLine label="Node key" value={attachNodeDialog.node.node_key} />
<StateLine label={t.activeCluster} value={selectedCluster?.name || selectedClusterId} />
</div>
<div className="checkGrid">
{roleOptions.map((role) => (
<label className="checkLine" key={role}>
<input type="checkbox" checked={attachNodeRoles.includes(role)} onChange={() => setAttachNodeRoles(toggleArrayValue(attachNodeRoles, role))} />
{roleDisplayLabel(role)}
</label>
))}
</div>
<div className="actions">
<button
className="primary"
onClick={() =>
void runAction(
async () => {
await client.attachExistingNode(selectedClusterId, attachNodeDialog.node.id, attachNodeRoles);
setAttachNodeDialog(null);
setAttachNodeRoles([]);
setNodeViewScope("cluster");
},
"Узел подключен к активному кластеру.",
)
}
>
{t.connectWithRoles}
</button>
<button onClick={() => setAttachNodeDialog(null)}>{t.cancel}</button>
</div>
</div>
</div>
)}
{nodeInfoDialog &&
(() => {
const activeMembership = nodeInfoDialog.memberships.find((membership) => membership.cluster.id === selectedClusterId);
const node = activeMembership?.node || nodeInfoDialog.node;
const latestHeartbeat = activeMembership ? (heartbeatsByNode[node.id] || [])[0] : undefined;
const activeRoles = activeMembership ? (rolesByNode[node.id] || []).filter((role) => role.status === "active") : [];
const desiredWorkloads = activeMembership ? desiredWorkloadsByNode[node.id] || [] : [];
const observedWorkloads = activeMembership ? workloadsByNode[node.id] || [] : [];
return (
<div className="modalBackdrop" role="presentation">
<div className="modalCard wide" role="dialog" aria-modal="true" aria-labelledby="node-info-title">
<div className="cardHead">
<div>
<h3 id="node-info-title">
{nodeInfoMode === "manage" ? t.manageNode : t.nodeDetails}: {node.name}
</h3>
<p className="muted">{node.node_key}</p>
</div>
<button
className="ghost"
onClick={() => {
setNodeInfoDialog(null);
setNodeInfoMode("details");
}}
>
{t.close}
</button>
</div>
<section className="nodePanel">
<h4>{t.nodeIdentity}</h4>
<div className="stateList">
<StateLine label="Node ID" value={shortId(node.id)} />
<StateLine label="Ключ узла" value={node.node_key} />
<StateLine label="Тип владения" value={statusLabel(node.ownership_type)} />
<StateLine label="Owner org" value={shortId(node.owner_organization_id)} />
<StateLine label="Регистрация" value={statusLabel(node.registration_status)} />
<StateLine label="Здоровье" value={statusLabel(node.health_status)} />
<StateLine label="Версия" value={node.reported_version || "неизвестно"} />
<StateLine label="Последний сигнал" value={formatDate(node.last_seen_at)} />
</div>
</section>
<section className="nodePanel">
<h4>{t.clusterMemberships}</h4>
<div className="membershipList">
{nodeInfoDialog.memberships.map((membership) => (
<span key={membership.cluster.id} className={membership.cluster.id === selectedClusterId ? "pill good" : "pill"}>
{membership.cluster.name}: {statusLabel(membership.node.membership_status)}
</span>
))}
</div>
</section>
{activeMembership ? (
<>
<section className="nodePanel">
<h4>{t.activeClusterScope}</h4>
<div className="stateList">
<StateLine label="Участие" value={statusLabel(node.membership_status)} />
<StateLine label="Сегмент" value={statusLabel(node.partition_state)} />
<StateLine label="Группа" value={node.node_group_name || t.ungroupedNodes} />
<StateLine label="Ролей" value={String(activeRoles.length)} />
<StateLine label="Desired-сервисов" value={String(desiredWorkloads.length)} />
<StateLine label="Observed-сервисов" value={String(observedWorkloads.length)} />
</div>
</section>
{nodeInfoMode === "details" && (
<NodeDetailsDashboard
node={node}
memberships={nodeInfoDialog.memberships}
activeRoles={activeRoles}
desiredWorkloads={desiredWorkloads}
observedWorkloads={observedWorkloads}
heartbeats={heartbeatsByNode[node.id] || []}
telemetry={telemetryByNode[node.id] || []}
updatePlan={nodeUpdatePlansByNode[node.id]}
updateStatuses={nodeUpdateStatusesByNode[node.id] || []}
meshLinks={meshLinks.filter((link) => link.source_node_id === node.id || link.target_node_id === node.id)}
syntheticConfig={syntheticMeshConfigsByNode[node.id]}
allNodes={nodes}
onSetUpdatePolicy={(targetNode, product, targetVersion) =>
void runAction(async () => {
await client.upsertNodeUpdatePolicy(selectedClusterId, targetNode.id, {
product,
channel: "dev",
targetVersion,
strategy: targetVersion ? "rolling" : "rolling",
enabled: true,
rollbackAllowed: true,
healthWindowSeconds: 180,
});
}, targetVersion ? `${product} поставлен в target ${targetVersion}.` : `${product} будет следовать latest dev.`)
}
labels={t}
/>
)}
{nodeInfoMode === "manage" && (
<section className="nodePanel">
<h4>{t.nodeFunctions}</h4>
<p className="muted">{t.nodeFunctionsText}</p>
<label className="wideLabel">
{t.organizationScopeForEnable}
<input value={roleOrgScope} onChange={(event) => setRoleOrgScope(event.target.value)} placeholder={t.clusterWideRolePlaceholder} />
</label>
<div className="functionList">
{roleOptions.map((role) => {
const activeRole = activeRoles.find((assignment) => assignment.role === role);
const desired = desiredWorkloads.find((workload) => workload.service_type === role);
const observed = observedWorkloads.find((workload) => workload.service_type === role);
const capabilityTone = capabilityPillTone(role, latestHeartbeat);
const desiredState = desired?.desired_state || "not_configured";
const observedState = observed?.reported_state || "missing";
const enabled = Boolean(activeRole) && desiredState === "enabled";
return (
<div className="functionRow" key={role}>
<div className="nodeListMain">
<strong>{roleDisplayLabel(role)}</strong>
<span>{capabilityOptionLabel(role, latestHeartbeat, language)}</span>
</div>
<FunctionState label={t.rolePermission} value={activeRole ? t.permissionGranted : t.permissionDenied} tone={activeRole ? "info" : ""} />
<FunctionState label={t.desiredRuntime} value={statusLabel(desiredState)} tone={desiredState === "enabled" ? "good" : ""} />
<FunctionState label={t.observedRuntime} value={statusLabel(observedState)} tone={observedState === "running" ? "good" : observedState === "missing" ? "warn" : ""} />
<span className={`pill ${capabilityTone}`}>{capabilityLabel(role, latestHeartbeat, t)}</span>
<div className="actions">
<button
className={enabled ? "" : "primary"}
disabled={enabled}
onClick={() =>
void runAction(async () => {
if (!activeRole) {
await client.setRoleStatus(selectedClusterId, node.id, role, "active", roleOrgScope || undefined);
}
await client.setDesiredWorkload(selectedClusterId, node.id, role, {
desiredState: "enabled",
runtimeMode: "container",
config: {},
environment: {},
});
}, `${role}: функция включена.`)
}
>
{t.enableFunction}
</button>
<button
disabled={!activeRole && desiredState !== "enabled"}
onClick={() =>
void runAction(async () => {
await client.setDesiredWorkload(selectedClusterId, node.id, role, {
desiredState: "disabled",
runtimeMode: desired?.runtime_mode || "container",
config: desired?.config || {},
environment: desired?.environment || {},
});
if (activeRole) {
await client.setRoleStatus(selectedClusterId, node.id, role, "disabled", activeRole.organization_id || undefined);
}
}, `${role}: функция выключена.`)
}
>
{t.disableFunction}
</button>
</div>
</div>
);
})}
</div>
{(() => {
const listenerDesired = desiredWorkloads.find((workload) => workload.service_type === "mesh-listener");
const listenerConfig = (listenerDesired?.config || {}) as Record<string, unknown>;
const draft =
meshListenerDrafts[node.id] || {
listenAddr: String(listenerConfig.listen_addr || ":19131"),
mode: String(listenerConfig.listen_port_mode || "auto"),
autoRange: `${Number(listenerConfig.auto_port_start || 19131)}-${Number(listenerConfig.auto_port_end || 19231)}`,
advertiseEndpoint: String(listenerConfig.advertise_endpoint || ""),
advertiseTransport: String(listenerConfig.advertise_transport || "direct_http"),
connectivity: String(listenerConfig.connectivity_mode || "private_lan"),
nat: String(listenerConfig.nat_type || "none"),
region: String(listenerConfig.region || ""),
};
const setDraft = (patch: Partial<typeof draft>) => setMeshListenerDrafts({ ...meshListenerDrafts, [node.id]: { ...draft, ...patch } });
return (
<section className="nodePanel nestedPanel">
<h4>Mesh listener</h4>
<FormGrid>
<label>
Listen addr
<input value={draft.listenAddr} onChange={(event) => setDraft({ listenAddr: event.target.value })} placeholder="0.0.0.0:19131 или :19131" />
</label>
<label>
Port mode
<select value={draft.mode} onChange={(event) => setDraft({ mode: event.target.value })}>
<option value="auto">auto</option>
<option value="manual">manual</option>
<option value="disabled">disabled</option>
</select>
</label>
<label>
Auto ports
<input value={draft.autoRange} onChange={(event) => setDraft({ autoRange: event.target.value })} placeholder="19131-19231" />
</label>
<label>
Advertise endpoint
<input
value={draft.advertiseEndpoint}
onChange={(event) => setDraft({ advertiseEndpoint: event.target.value })}
placeholder="http://external-or-lan-ip:19131"
/>
</label>
<label>
Advertise transport
<select value={draft.advertiseTransport} onChange={(event) => setDraft({ advertiseTransport: event.target.value })}>
<option value="direct_http">direct_http</option>
<option value="direct_https">direct_https</option>
<option value="wss">wss</option>
</select>
</label>
<label>
Connectivity
<select value={draft.connectivity} onChange={(event) => setDraft({ connectivity: event.target.value })}>
<option value="private_lan">private_lan</option>
<option value="direct">direct</option>
<option value="outbound_only">outbound_only</option>
<option value="relay_required">relay_required</option>
</select>
</label>
<label>
NAT
<select value={draft.nat} onChange={(event) => setDraft({ nat: event.target.value })}>
<option value="none">none</option>
<option value="unknown">unknown</option>
<option value="port_restricted">port_restricted</option>
<option value="symmetric">symmetric</option>
</select>
</label>
<label>
Region/site
<input value={draft.region} onChange={(event) => setDraft({ region: event.target.value })} placeholder="dc1, office, docker-test" />
</label>
</FormGrid>
<div className="actions">
<button
className="primary"
onClick={() =>
void runAction(async () => {
const [startRaw, endRaw] = draft.autoRange.split("-").map((value) => Number(value.trim()));
const start = Number.isFinite(startRaw) ? startRaw : 19131;
const end = Number.isFinite(endRaw) ? endRaw : start;
await client.setDesiredWorkload(selectedClusterId, node.id, "mesh-listener", {
desiredState: draft.mode === "disabled" ? "disabled" : "enabled",
version: `listener-${Date.now()}`,
runtimeMode: "container",
config: {
listen_addr: draft.listenAddr,
listen_port_mode: draft.mode,
auto_port_start: start,
auto_port_end: end,
advertise_endpoint: draft.advertiseEndpoint.trim().replace(/\/$/, "") || null,
advertise_transport: draft.advertiseTransport || "direct_http",
connectivity_mode: draft.connectivity,
nat_type: draft.nat,
region: draft.region || null,
},
environment: {},
});
}, "Mesh listener config обновлен.")
}
>
Применить listener
</button>
</div>
</section>
);
})()}
<div className="actions">
<select
value={node.node_group_id || ""}
onChange={(event) =>
void runAction(
() => client.assignNodeGroup(selectedClusterId, node.id, event.target.value || null),
event.target.value ? "Узел перемещен в группу." : "Узел убран из группы.",
)
}
>
<option value="">{t.ungroupedNodes}</option>
{nodeGroups.map((group) => (
<option key={group.id} value={group.id}>
{groupPathLabel(group, nodeGroups)}
</option>
))}
</select>
</div>
<div className="actions">
<button
onClick={() =>
confirmHighRisk(`Отключить участие узла ${node.name}`) &&
void runAction(
() => client.disableMembership(selectedClusterId, node.id, "Отключено из панели владельца платформы."),
"Участие узла отключено.",
)
}
>
Отключить участие
</button>
<button
className="danger"
onClick={() =>
confirmHighRisk(`Отозвать identity узла ${node.name}`) &&
void runAction(
() => client.revokeNodeIdentity(selectedClusterId, node.id, "Отозвано из панели владельца платформы."),
"Identity узла отозван.",
)
}
>
Отозвать identity
</button>
</div>
</section>
)}
</>
) : (
<section className="nodePanel">
<h4>{t.noActiveClusterMembership}</h4>
<div className="actions">
<button
className="primary"
onClick={() => {
setAttachNodeDialog(nodeInfoDialog);
setAttachNodeRoles([]);
setNodeInfoDialog(null);
}}
>
{t.connectExistingNode}
</button>
</div>
</section>
)}
</div>
</div>
);
})()}
{false && (nodeViewScope === "all" ? (
<article className="card span2">
<h3>{t.allNodes}</h3>
<p className="muted">{t.nodeGlobalInventoryText}</p>
<FormGrid>
<label>
{t.nodeSearch}
<input value={allNodeSearch} onChange={(event) => setAllNodeSearch(event.target.value)} placeholder="имя, ключ, кластер, статус" />
</label>
<label>
{t.groupNodesBy}
<select value={allNodeGroupBy} onChange={(event) => setAllNodeGroupBy(event.target.value as typeof allNodeGroupBy)}>
<option value="membership">{t.groupByMembership}</option>
<option value="health">{t.groupByHealth}</option>
<option value="ownership">{t.groupByOwnership}</option>
<option value="cluster_count">{t.groupByClusterCount}</option>
</select>
</label>
</FormGrid>
<div className="stack">
{groupedAllNodeInventory.map((group) => (
<section className="nodePanel" key={group.label}>
<div className="cardHead">
<h4>{group.label}</h4>
<span className="pill">{group.items.length}</span>
</div>
<DataTable
columns={["узел", "ключ", t.currentClusterMembership, t.clusterMemberships, "здоровье", "версия", "последний сигнал", "действие"]}
rows={group.items.map((entry) => {
const { node, memberships } = entry;
const activeMembership = memberships.find((membership) => membership.cluster.id === selectedClusterId);
const isActiveMember = activeMembership?.node.membership_status === "active";
const isRevokedMember = activeMembership?.node.membership_status === "revoked";
return [
node.name,
node.node_key,
activeMembership ? <Status value={activeMembership.node.membership_status} /> : <span className="muted">{t.notMemberOfActiveCluster}</span>,
<div className="membershipList">
{memberships.map((membership) => (
<span key={`${membership.cluster.id}-${membership.node.id}`} className={membership.cluster.id === selectedClusterId ? "pill good" : "pill"}>
{membership.cluster.name}: {statusLabel(membership.node.membership_status)}
</span>
))}
</div>,
statusLabel(node.health_status),
node.reported_version || "неизвестно",
formatDate(node.last_seen_at),
<div className="actions" key={`${selectedClusterId}-${node.id}`}>
{isActiveMember ? (
<button
onClick={() => {
setNodeViewScope("cluster");
}}
>
{t.manageInCluster}
</button>
) : isRevokedMember ? (
<span className="muted">{t.revokedMembership}</span>
) : (
<button
className="primary"
onClick={() => {
setAttachNodeDialog(entry);
setAttachNodeRoles([]);
}}
>
{t.connectExistingNode}
</button>
)}
</div>,
];
})}
/>
</section>
))}
{groupedAllNodeInventory.length === 0 && <EmptyState title="Нет узлов" text="По текущему фильтру узлы не найдены." />}
</div>
{attachNodeDialog && (
<div className="modalBackdrop" role="presentation">
<div className="modalCard" role="dialog" aria-modal="true" aria-labelledby="attach-node-title">
<div className="cardHead">
<div>
<h3 id="attach-node-title">{t.connectExistingNodeTitle}</h3>
<p className="muted">{t.connectExistingNodeText}</p>
</div>
<button className="ghost" onClick={() => setAttachNodeDialog(null)}>
{t.cancel}
</button>
</div>
<div className="stateList">
<StateLine label="Узел" value={attachNodeDialog!.node.name} />
<StateLine label="Node key" value={attachNodeDialog!.node.node_key} />
<StateLine label={t.activeCluster} value={selectedCluster?.name || selectedClusterId} />
</div>
<div className="checkGrid">
{roleOptions.map((role) => (
<label className="checkLine" key={role}>
<input
type="checkbox"
checked={attachNodeRoles.includes(role)}
onChange={() => setAttachNodeRoles(toggleArrayValue(attachNodeRoles, role))}
/>
{roleDisplayLabel(role)}
</label>
))}
</div>
<div className="actions">
<button
className="primary"
onClick={() =>
void runAction(
async () => {
await client.attachExistingNode(selectedClusterId, attachNodeDialog!.node.id, attachNodeRoles);
setAttachNodeDialog(null);
setAttachNodeRoles([]);
setNodeViewScope("cluster");
},
"Узел подключен к активному кластеру.",
)
}
>
{t.connectWithRoles}
</button>
<button onClick={() => setAttachNodeDialog(null)}>{t.cancel}</button>
</div>
</div>
</div>
)}
</article>
) : (
<>
<article className="card span2">
<h3>{t.testMode}</h3>
<p className="muted">{t.testModeText}</p>
<div className="actions">
<button
className={platformTestingFlag?.telemetry_enabled ? "primary" : ""}
onClick={() =>
void runAction(
() =>
client.updateFabricTestingFlag({
scopeType: "platform",
enabled: true,
telemetryEnabled: !platformTestingFlag?.telemetry_enabled,
syntheticLinksEnabled: Boolean(platformTestingFlag?.synthetic_links_enabled),
historyRetentionHours: platformTestingFlag?.history_retention_hours || 24,
}),
platformTestingFlag?.telemetry_enabled ? "Тестовая телеметрия платформы выключена." : "Тестовая телеметрия платформы включена.",
)
}
>
{t.enableTelemetry}: {platformTestingFlag?.telemetry_enabled ? "ВКЛ" : "ВЫКЛ"}
</button>
<button
className={platformTestingFlag?.synthetic_links_enabled ? "primary" : ""}
onClick={() =>
void runAction(
() =>
client.updateFabricTestingFlag({
scopeType: "platform",
enabled: true,
telemetryEnabled: Boolean(platformTestingFlag?.telemetry_enabled),
syntheticLinksEnabled: !platformTestingFlag?.synthetic_links_enabled,
historyRetentionHours: platformTestingFlag?.history_retention_hours || 24,
}),
platformTestingFlag?.synthetic_links_enabled ? "Тестовые связи выключены." : "Тестовые связи включены.",
)
}
>
{t.enableSyntheticLinks}: {platformTestingFlag?.synthetic_links_enabled ? "ВКЛ" : "ВЫКЛ"}
</button>
</div>
<section className="nodePanel">
<h4>{t.organizationTestFlag}</h4>
<p className="muted">
Флаг уровня организации нужен для безопасной проверки телеметрии и истории в tenant scope. Он не раскрывает внутреннюю topology
organization-панелям.
</p>
<FormGrid>
<label>
{t.organizationId}
<input value={testingOrgId} onChange={(event) => setTestingOrgId(event.target.value)} placeholder="UUID организации" />
</label>
<label className="checkLine">
<input
type="checkbox"
checked={testingOrgDraft.telemetry}
onChange={(event) => setTestingOrgDraft({ ...testingOrgDraft, telemetry: event.target.checked })}
/>
{t.enableTelemetry}
</label>
<label className="checkLine">
<input
type="checkbox"
checked={testingOrgDraft.links}
onChange={(event) => setTestingOrgDraft({ ...testingOrgDraft, links: event.target.checked })}
/>
{t.enableSyntheticLinks}
</label>
</FormGrid>
<div className="actions">
<button
disabled={!testingOrgId}
onClick={() =>
void runAction(
() =>
client.updateFabricTestingFlag({
scopeType: "organization",
scopeId: testingOrgId,
clusterId: selectedClusterId,
enabled: true,
telemetryEnabled: testingOrgDraft.telemetry,
syntheticLinksEnabled: testingOrgDraft.links,
historyRetentionHours: organizationTestingFlag?.history_retention_hours || 24,
}),
"Флаг тестирования организации сохранен.",
)
}
>
{t.saveOrganizationFlag}
</button>
{organizationTestingFlag && <Status value={organizationTestingFlag?.enabled ? "enabled" : "disabled"} />}
</div>
</section>
</article>
<article className="card span2">
<div className="cardHead">
<div>
<h3>{t.nodeGroups}</h3>
<p className="muted">
Группы принадлежат активному кластеру и могут быть вложенными. Один узел может быть в другой группе в другом кластере.
</p>
</div>
<span className="pill">{nodeGroups.length}</span>
</div>
<FormGrid>
<label>
{t.nodeGroupName}
<input value={nodeGroupForm.name} onChange={(event) => setNodeGroupForm({ ...nodeGroupForm, name: event.target.value })} />
</label>
<label>
{t.parentNodeGroup}
<select
value={nodeGroupForm.parentGroupId}
onChange={(event) => setNodeGroupForm({ ...nodeGroupForm, parentGroupId: event.target.value })}
>
<option value="">{t.rootNodeGroup}</option>
{nodeGroups.map((group) => (
<option key={group.id} value={group.id}>
{groupPathLabel(group, nodeGroups)}
</option>
))}
</select>
</label>
</FormGrid>
<div className="actions">
<button
className="primary"
disabled={!nodeGroupForm.name.trim()}
onClick={() =>
void runAction(
async () => {
await client.createNodeGroup(selectedClusterId, {
name: nodeGroupForm.name,
parentGroupId: nodeGroupForm.parentGroupId || null,
});
setNodeGroupForm({ name: "", parentGroupId: "" });
},
"Группа узлов создана.",
)
}
>
{t.createNodeGroup}
</button>
</div>
<NodeGroupTree groups={nodeGroups} nodes={nodes} labels={t} />
</article>
{nodes.map((node) => {
const latestHeartbeat = (heartbeatsByNode[node.id] || [])[0];
const activeRoles = activeRoleAssignments(rolesByNode[node.id] || []);
const desiredWorkloads = desiredWorkloadsByNode[node.id] || [];
const observedWorkloads = workloadsByNode[node.id] || [];
return (
<article className="card nodeCard" key={node.id}>
<div className="cardHead">
<div>
<h3>{node.name}</h3>
<p className="muted">{node.node_key}</p>
</div>
<Status value={node.health_status} />
</div>
<section className="nodePanel">
<h4>{t.nodeIdentity}</h4>
<div className="stateList">
<StateLine label="Node ID" value={shortId(node.id)} />
<StateLine label="Ключ узла" value={node.node_key} />
<StateLine label="Тип владения" value={statusLabel(node.ownership_type)} />
<StateLine label="Owner org" value={shortId(node.owner_organization_id)} />
<StateLine label="Регистрация" value={statusLabel(node.registration_status)} />
</div>
</section>
<section className="nodePanel">
<h4>{t.activeClusterScope}</h4>
<p className="muted">{t.activeClusterScopeText}</p>
<div className="stateList">
<StateLine label="Участие" value={statusLabel(node.membership_status)} />
<StateLine label="Сегмент" value={statusLabel(node.partition_state)} />
<StateLine label="Здоровье" value={statusLabel(node.health_status)} />
<StateLine label="Группа" value={node.node_group_name || t.ungroupedNodes} />
<StateLine label="Версия" value={node.reported_version || "неизвестно"} />
<StateLine label="Последний сигнал" value={formatDate(node.last_seen_at)} />
<StateLine label="Ролей в кластере" value={String(activeRoles.length)} />
<StateLine label="Desired-сервисов" value={String(desiredWorkloads.length)} />
<StateLine label="Observed-сервисов" value={String(observedWorkloads.length)} />
</div>
</section>
<section className="nodePanel">
<h4>{t.nodeRoles}</h4>
<div className="serviceTags">
{activeRoles.length === 0 && <p className="muted">{t.noRoles}</p>}
{activeRoles.map((role) => (
<div className="serviceTag" key={role.id}>
<strong>{roleDisplayLabel(role.role)}</strong>
<span>{role.organization_id ? `organization: ${shortId(role.organization_id)}` : "cluster-wide"}</span>
<small>{formatDate(role.assigned_at)}</small>
<span className={`pill ${capabilityPillTone(role.role, latestHeartbeat)}`}>
{capabilityLabel(role.role, latestHeartbeat, t)}
</span>
</div>
))}
</div>
</section>
<section className="nodePanel">
<h4>{t.desiredServices}</h4>
<div className="serviceTags">
{desiredWorkloads.length === 0 && <p className="muted">{t.noServices}</p>}
{desiredWorkloads.map((workload) => (
<div className="serviceTag" key={`${workload.service_type}-${workload.runtime_mode}`}>
<strong>{workload.service_type}</strong>
<span>{statusLabel(workload.desired_state)} / {workload.runtime_mode}</span>
<small>{workload.version || "версия не закреплена"}</small>
<span className={`pill ${capabilityPillTone(workload.service_type, latestHeartbeat)}`}>
{capabilityLabel(workload.service_type, latestHeartbeat, t)}
</span>
</div>
))}
</div>
</section>
<section className="nodePanel">
<h4>{t.observedServices}</h4>
<div className="serviceTags">
{observedWorkloads.length === 0 && <p className="muted">{t.noServices}</p>}
{observedWorkloads.map((workload) => (
<div className="serviceTag" key={workload.id}>
<strong>{workload.service_type}</strong>
<span>{statusLabel(workload.reported_state)} / {workload.runtime_mode}</span>
<small>{formatDate(workload.observed_at)}</small>
</div>
))}
</div>
</section>
<section className="nodePanel">
<h4>{t.nodeManagement}</h4>
<div className="actions">
<select
value={nodeRoleDrafts[node.id] || ""}
onChange={(event) => setNodeRoleDrafts({ ...nodeRoleDrafts, [node.id]: event.target.value })}
>
<option value="">Роль...</option>
{roleOptions.map((role) => (
<option key={role} value={role}>
{roleDisplayLabel(role)} {capabilityOptionLabel(role, latestHeartbeat, language)}
</option>
))}
</select>
<button
disabled={!nodeRoleDrafts[node.id]}
onClick={() =>
void runAction(
() => client.assignRole(selectedClusterId, node.id, nodeRoleDrafts[node.id], roleOrgScope || undefined),
"Роль назначена.",
)
}
>
Назначить роль
</button>
<select
value={nodeWorkloadDrafts[node.id] || ""}
onChange={(event) => setNodeWorkloadDrafts({ ...nodeWorkloadDrafts, [node.id]: event.target.value })}
>
<option value="">Сервис...</option>
{roleOptions.map((role) => (
<option key={role} value={role}>
{roleDisplayLabel(role)} {capabilityOptionLabel(role, latestHeartbeat, language)}
</option>
))}
</select>
<button
disabled={!nodeWorkloadDrafts[node.id]}
onClick={() =>
void runAction(
() =>
client.setDesiredWorkload(selectedClusterId, node.id, nodeWorkloadDrafts[node.id], {
desiredState: "enabled",
runtimeMode: "container",
config: {},
environment: {},
}),
"Желаемое состояние сервиса включено.",
)
}
>
Включить сервис
</button>
<button
disabled={!nodeWorkloadDrafts[node.id]}
onClick={() =>
void runAction(
() =>
client.setDesiredWorkload(selectedClusterId, node.id, nodeWorkloadDrafts[node.id], {
desiredState: "disabled",
runtimeMode: "container",
config: {},
environment: {},
}),
"Желаемое состояние сервиса выключено.",
)
}
>
Выключить сервис
</button>
<select
value={node.node_group_id || ""}
onChange={(event) =>
void runAction(
() => client.assignNodeGroup(selectedClusterId, node.id, event.target.value || null),
event.target.value ? "Узел перемещен в группу." : "Узел убран из группы.",
)
}
>
<option value="">{t.ungroupedNodes}</option>
{nodeGroups.map((group) => (
<option key={group.id} value={group.id}>
{groupPathLabel(group, nodeGroups)}
</option>
))}
</select>
</div>
</section>
<section className="nodePanel">
<h4>{t.testMode}</h4>
<div className="actions">
<label className="checkLine">
<input
type="checkbox"
checked={nodeTestingDrafts[node.id]?.telemetry ?? true}
onChange={(event) =>
setNodeTestingDrafts({
...nodeTestingDrafts,
[node.id]: { telemetry: event.target.checked, links: nodeTestingDrafts[node.id]?.links ?? true },
})
}
/>
{t.enableTelemetry}
</label>
<label className="checkLine">
<input
type="checkbox"
checked={nodeTestingDrafts[node.id]?.links ?? true}
onChange={(event) =>
setNodeTestingDrafts({
...nodeTestingDrafts,
[node.id]: { telemetry: nodeTestingDrafts[node.id]?.telemetry ?? true, links: event.target.checked },
})
}
/>
{t.enableSyntheticLinks}
</label>
<button
onClick={() =>
void runAction(
() =>
client.updateFabricTestingFlag({
scopeType: "node",
scopeId: node.id,
clusterId: selectedClusterId,
enabled: true,
telemetryEnabled: nodeTestingDrafts[node.id]?.telemetry ?? true,
syntheticLinksEnabled: nodeTestingDrafts[node.id]?.links ?? true,
historyRetentionHours: 24,
}),
"Флаг тестирования узла сохранен.",
)
}
>
{t.saveTestFlag}
</button>
</div>
</section>
<section className="nodePanel">
<h4>{t.nodeTelemetry}</h4>
<TelemetryStrip items={telemetryByNode[node.id] || []} emptyText={t.noTelemetry} />
</section>
<section className="nodePanel">
<h4>{t.recentHeartbeats}</h4>
<DataTable
columns={["состояние", "версия", "listener", "mesh recovery", "mesh intents", "rv leases", "path decisions", "route gen", "route health", "наблюдение"]}
rows={(heartbeatsByNode[node.id] || []).slice(0, 5).map((heartbeat) => [
heartbeat.health_status,
heartbeat.reported_version || "неизвестно",
meshListenerSummary(heartbeat),
meshRecoverySummary(heartbeat),
meshConnectionIntentSummary(heartbeat),
meshRendezvousLeaseSummary(heartbeat),
meshRoutePathDecisionSummary(heartbeat),
meshRouteGenerationSummary(heartbeat),
meshRouteHealthConfigSummary(heartbeat),
formatDate(heartbeat.observed_at),
])}
/>
</section>
<div className="actions">
<button
onClick={() =>
confirmHighRisk(`Отключить участие узла ${node.name}`) &&
void runAction(
() => client.disableMembership(selectedClusterId, node.id, "Отключено из панели владельца платформы."),
"Участие узла отключено.",
)
}
>
Отключить участие
</button>
<button
className="danger"
onClick={() =>
confirmHighRisk(`Отозвать identity узла ${node.name}`) &&
void runAction(
() => client.revokeNodeIdentity(selectedClusterId, node.id, "Отозвано из панели владельца платформы."),
"Identity узла отозван.",
)
}
>
Отозвать identity
</button>
</div>
</article>
);
})}
{nodes.length === 0 && <EmptyState title="Нет узлов" text="Одобренные node-agent появятся здесь." />}
</>
))}
</section>
)}
{activeView === "enrollment" && (
<section className="grid two">
<article className="card">
<h3>{t.joinTokenTitle}</h3>
<p className="muted">{t.joinTokenText}</p>
<FormGrid>
<label>
{t.ttlHours}
<input
type="number"
min={1}
max={720}
value={joinTokenForm.ttlHours}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, ttlHours: Number(event.target.value) })}
/>
<small>{t.ttlHelp}</small>
</label>
<label>
{t.maxUses}
<input
type="number"
min={1}
max={100}
value={joinTokenForm.maxUses}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, maxUses: Number(event.target.value) })}
/>
<small>{t.maxUsesHelp}</small>
</label>
<label>
{t.nodeOwnership}
<select
value={joinTokenForm.ownershipType}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, ownershipType: event.target.value })}
>
<option value="platform_managed">platform_managed, управляется платформой</option>
<option value="customer_managed">customer_managed, управляется клиентом</option>
</select>
</label>
<label>
{t.tokenPurpose}
<input
value={joinTokenForm.purpose}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, purpose: event.target.value })}
placeholder="например: стартовый entry-node в ru-msk-1"
/>
</label>
<label>
Имя нового узла
<input
value={joinTokenForm.nodeName}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, nodeName: event.target.value })}
placeholder={suggestedInstallNodeName(joinTokenForm, selectedCluster)}
/>
<small>Если оставить пустым, панель подставит имя автоматически.</small>
</label>
<label>
Группа узла
<select value={joinTokenForm.nodeGroupId} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, nodeGroupId: event.target.value })}>
<option value="">Без группы</option>
{nodeGroups.map((group) => (
<option key={group.id} value={group.id}>
{groupPathLabel(group, nodeGroups)}
</option>
))}
</select>
</label>
</FormGrid>
<section className="nodePanel">
<h4>Install profile</h4>
<p className="muted">Эти поля попадут в install profile. Для Windows без админ-прав будет создан user startup task, с админ-правами - system startup task.</p>
<div className="segmented">
{[
["docker", "Docker Linux"],
["linux_binary", "Ubuntu service"],
["windows_service", "Windows"],
].map(([mode, label]) => (
<button
type="button"
key={mode}
className={joinTokenForm.installMode === mode ? "active" : ""}
onClick={() => setJoinTokenForm({ ...joinTokenForm, installMode: mode })}
>
{label}
</button>
))}
</div>
<div className="segmented">
{[
["private_lan", "LAN"],
["direct", "Public"],
["nat_forward", "NAT"],
["outbound_only", "Outbound"],
].map(([mode, label]) => (
<button
type="button"
key={mode}
className={dockerConnectivityPreset(joinTokenForm) === mode ? "active" : ""}
onClick={() => setJoinTokenForm(applyDockerConnectivityPreset(joinTokenForm, mode))}
>
{label}
</button>
))}
</div>
<FormGrid>
<label>
Control-plane endpoint
<input
value={joinTokenForm.controlPlaneEndpoint}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, controlPlaneEndpoint: event.target.value })}
placeholder={defaultControlPlaneEndpoint()}
/>
</label>
<label>
{joinTokenForm.installMode === "windows_service" ? "Windows node-agent artifact" : joinTokenForm.installMode === "linux_binary" ? "Linux node-agent artifact" : "Docker image"}
<input value={joinTokenForm.dockerImage} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, dockerImage: event.target.value })} />
</label>
{joinTokenForm.installMode === "windows_service" && (
<>
<label>
Windows startup
<select value={joinTokenForm.windowsStartupMode} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, windowsStartupMode: event.target.value })}>
<option value="auto">auto: system task, fallback user task</option>
<option value="system-task">system task, admin required</option>
<option value="user-task">user task, no admin</option>
<option value="none">none</option>
</select>
</label>
<label>
Install dir
<input
value={joinTokenForm.windowsInstallDir}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, windowsInstallDir: event.target.value })}
placeholder="C:\\Program Files\\RAP\\node-name"
/>
</label>
<label>
Windows node-agent SHA256
<input
value={joinTokenForm.windowsNodeAgentSHA256}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, windowsNodeAgentSHA256: event.target.value })}
placeholder="опционально, но желательно для production"
/>
</label>
</>
)}
{joinTokenForm.installMode === "linux_binary" && (
<>
<label>
Linux install dir
<input
value={joinTokenForm.linuxInstallDir}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, linuxInstallDir: event.target.value })}
placeholder="/opt/rap/node-name"
/>
</label>
<label>
Linux node-agent SHA256
<input
value={joinTokenForm.linuxNodeAgentSHA256}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, linuxNodeAgentSHA256: event.target.value })}
placeholder="опционально, но желательно для production"
/>
</label>
</>
)}
{joinTokenForm.installMode === "docker" && <label>
Container name
<input
value={joinTokenForm.dockerContainerName}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, dockerContainerName: event.target.value })}
placeholder={suggestedInstallContainerName(joinTokenForm, selectedCluster)}
/>
</label>}
<label>
Artifact endpoints
<input
value={joinTokenForm.artifactEndpoints}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, artifactEndpoints: event.target.value })}
placeholder={defaultArtifactEndpoint()}
/>
<small>Через запятую: public/LAN/cache узлы, где host-agent сможет скачать image tar до входа в mesh.</small>
</label>
{joinTokenForm.installMode === "docker" && <label>
Docker image tar SHA256
<input
value={joinTokenForm.dockerImageArtifactSHA256}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, dockerImageArtifactSHA256: event.target.value })}
placeholder="опционально, но желательно для production"
/>
</label>}
{joinTokenForm.installMode === "docker" && <label>
Docker network
<select value={joinTokenForm.dockerNetwork} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, dockerNetwork: event.target.value })}>
<option value="host">host</option>
<option value="bridge">bridge</option>
</select>
</label>}
<label>
Listen addr
<input value={joinTokenForm.meshListenAddr} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, meshListenAddr: event.target.value })} />
</label>
<label>
Listen mode
<select
value={joinTokenForm.meshListenPortMode}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, meshListenPortMode: event.target.value })}
>
<option value="auto">auto</option>
<option value="manual">manual</option>
<option value="disabled">disabled</option>
</select>
</label>
<label>
Auto ports
<input
value={`${joinTokenForm.meshListenAutoPortStart}-${joinTokenForm.meshListenAutoPortEnd}`}
onChange={(event) => {
const [start, end] = event.target.value.split("-").map((value) => Number(value.trim()));
setJoinTokenForm({
...joinTokenForm,
meshListenAutoPortStart: Number.isFinite(start) ? start : joinTokenForm.meshListenAutoPortStart,
meshListenAutoPortEnd: Number.isFinite(end) ? end : joinTokenForm.meshListenAutoPortEnd,
});
}}
/>
</label>
<label>
Advertise endpoint
<input
value={joinTokenForm.meshAdvertiseEndpoint}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, meshAdvertiseEndpoint: event.target.value })}
placeholder="http://public-or-private-ip:19131"
/>
</label>
<label>
Connectivity
<select
value={joinTokenForm.meshConnectivityMode}
onChange={(event) => setJoinTokenForm({ ...joinTokenForm, meshConnectivityMode: event.target.value })}
>
<option value="direct">direct</option>
<option value="private_lan">private_lan</option>
<option value="outbound_only">outbound_only</option>
<option value="relay_required">relay_required</option>
</select>
</label>
<label>
NAT
<select value={joinTokenForm.meshNATType} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, meshNATType: event.target.value })}>
<option value="none">none</option>
<option value="unknown">unknown</option>
<option value="full_cone">full_cone</option>
<option value="port_restricted">port_restricted</option>
<option value="symmetric">symmetric</option>
</select>
</label>
<label>
Region/site
<input value={joinTokenForm.meshRegion} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, meshRegion: event.target.value })} />
</label>
{joinTokenForm.installMode === "docker" && <label className="checkLine">
<input type="checkbox" checked={joinTokenForm.pullImage} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, pullImage: event.target.checked })} />
Pull image
</label>}
<label className="checkLine">
<input type="checkbox" checked={joinTokenForm.replace} onChange={(event) => setJoinTokenForm({ ...joinTokenForm, replace: event.target.checked })} />
Replace existing install
</label>
</FormGrid>
</section>
<section className="nodePanel">
<h4>{t.suggestedRoles}</h4>
<p className="muted">
Роли записываются в install token и автоматически назначаются узлу при approval. После создания token изменение чекбоксов не меняет уже выданный token.
</p>
<div className="checkGrid">
{roleOptions.map((role) => (
<label className="checkLine" key={role}>
<input
type="checkbox"
checked={joinTokenForm.roles.includes(role)}
onChange={() => setJoinTokenForm({ ...joinTokenForm, roles: toggleArrayValue(joinTokenForm.roles, role) })}
/>
{roleDisplayLabel(role)}
</label>
))}
</div>
</section>
<details>
<summary>{t.generatedScope}</summary>
<p className="muted">{t.generatedScopeHelp}</p>
<pre className="codePreview">{JSON.stringify(joinTokenScope, null, 2)}</pre>
</details>
<p className="muted">{t.manualApprovalRequired}.</p>
<button
className="primary"
disabled={!selectedClusterId}
onClick={() =>
void runAction(async () => {
const token = await client.createJoinToken(selectedClusterId, {
ttlHours: joinTokenForm.ttlHours,
maxUses: joinTokenForm.maxUses,
scope: joinTokenScope,
});
setLastJoinToken(token);
}, "Join token создан.")
}
>
Создать install token
</button>
{lastJoinToken && (
<div className="secretOnce">
<strong>Исходный token, возвращается один раз</strong>
<code>{lastJoinToken.token}</code>
<span className="muted">Authority key: {shortId(lastJoinToken.authority_signature?.key_fingerprint)}</span>
<strong>Scope выданного token</strong>
<pre className="codePreview">{JSON.stringify(lastJoinToken.scope, null, 2)}</pre>
<strong>Docker host-agent install</strong>
<pre className="codePreview">{hostAgentDockerInstallCommand(lastJoinToken, selectedCluster, lastJoinTokenInstallForm)}</pre>
<strong>Profile-based Docker install</strong>
<pre className="codePreview">{hostAgentDockerProfileInstallCommand(lastJoinToken, selectedCluster, lastJoinTokenInstallForm)}</pre>
<strong>Profile-based Ubuntu service install</strong>
<pre className="codePreview">{hostAgentLinuxProfileInstallCommand(lastJoinToken, selectedCluster, lastJoinTokenInstallForm)}</pre>
<strong>Profile-based Windows PowerShell install</strong>
<pre className="codePreview">{hostAgentWindowsProfileInstallCommand(lastJoinToken, selectedCluster, lastJoinTokenInstallForm)}</pre>
<strong>Profile-based Windows CMD install</strong>
<pre className="codePreview">{hostAgentWindowsCmdProfileInstallCommand(lastJoinToken, selectedCluster, lastJoinTokenInstallForm)}</pre>
</div>
)}
</article>
<article className="card">
<h3>Как добавить узел</h3>
<div className="stateList">
<div className="stateLine">
<span>1</span>
<strong>Заполните Docker install profile слева.</strong>
</div>
<div className="stateLine">
<span>2</span>
<strong>Нажмите Создать install token.</strong>
</div>
<div className="stateLine">
<span>3</span>
<strong>Скопируйте Profile-based Docker install и выполните на Docker-хосте.</strong>
</div>
<div className="stateLine">
<span>4</span>
<strong>Подтвердите join request в этой же вкладке.</strong>
</div>
</div>
</article>
<article className="card">
<h3>Install tokens</h3>
<DataTable
columns={["scope", "status", "uses", "expires", "created", "action"]}
rows={joinTokens.map((token) => [
joinTokenName(token),
statusLabel(token.status),
`${token.used_count}/${token.max_uses}`,
formatDate(token.expires_at),
formatDate(token.created_at),
token.status === "active" ? (
<button
className="danger"
onClick={() =>
confirmHighRisk(`Отозвать install token ${shortId(token.id)}`) &&
void runAction(() => client.revokeJoinToken(selectedClusterId, token.id), "Install token отозван.")
}
>
Отозвать
</button>
) : (
<span className="muted">{statusLabel(token.status)}</span>
),
])}
/>
</article>
<article className="card">
<h3>Заявки на подключение</h3>
<div className="stack">
{joinRequests.map((request) => (
<div className="requestCard" key={request.id}>
<div>
<strong>{request.node_name}</strong>
<p>{request.node_fingerprint}</p>
<Status value={request.status} />
{request.approval_signature?.key_fingerprint && (
<small className="muted">approval key {shortId(request.approval_signature.key_fingerprint)}</small>
)}
</div>
<div className="actions">
<button
disabled={request.status !== "pending"}
onClick={() => void runAction(() => client.approveJoinRequest(selectedClusterId, request.id), "Заявка одобрена.")}
>
Одобрить
</button>
<button
disabled={request.status !== "pending"}
onClick={() =>
void runAction(
() => client.rejectJoinRequest(selectedClusterId, request.id, "Отклонено из панели владельца платформы."),
"Заявка отклонена.",
)
}
>
Отклонить
</button>
</div>
</div>
))}
{joinRequests.length === 0 && <EmptyState title="Нет заявок" text="Новые подключения node-agent появятся здесь." />}
</div>
</article>
</section>
)}
{activeView === "roles" && (
<section className="stack">
<article className="card">
<h3>Область ролей</h3>
<p className="muted">Capabilities технические факты. Роли явные разрешения. Область организации может ограничивать сервисные роли.</p>
<label>
UUID организации для новых назначений ролей, опционально
<input value={roleOrgScope} onChange={(event) => setRoleOrgScope(event.target.value)} placeholder="пусто = роль на весь кластер" />
</label>
</article>
{nodes.map((node) => (
<article className="card roleRow" key={node.id}>
<div>
<h3>{node.name}</h3>
<p>{summarizeRoles(rolesByNode[node.id] || [])}</p>
</div>
<select
defaultValue=""
onChange={(event) => {
const role = event.target.value;
event.currentTarget.value = "";
if (role) {
void runAction(() => client.assignRole(selectedClusterId, node.id, role, roleOrgScope || undefined), `${role} назначена узлу ${node.name}.`);
}
}}
>
<option value="">Назначить роль...</option>
{roleOptions.map((role) => (
<option key={role} value={role}>
{roleDisplayLabel(role)}
</option>
))}
</select>
</article>
))}
</section>
)}
{activeView === "workloads" && (
<section className="grid two">
<article className="card">
<h3>Желаемое состояние сервиса</h3>
<p className="muted">Здесь задается только желаемое состояние. Runtime-исполнение остается под контролем node-agent и политик.</p>
<FormGrid>
<label>
Узел
<select value={workloadForm.nodeId} onChange={(event) => setWorkloadForm({ ...workloadForm, nodeId: event.target.value })}>
<option value="">Выберите узел...</option>
{nodes.map((node) => (
<option key={node.id} value={node.id}>
{node.name}
</option>
))}
</select>
</label>
<label>
Сервис
<select value={workloadForm.serviceType} onChange={(event) => setWorkloadForm({ ...workloadForm, serviceType: event.target.value })}>
{roleOptions.map((role) => (
<option key={role} value={role}>
{roleDisplayLabel(role)}
</option>
))}
</select>
</label>
<label>
Желаемое состояние
<select value={workloadForm.desiredState} onChange={(event) => setWorkloadForm({ ...workloadForm, desiredState: event.target.value })}>
<option value="enabled">включено</option>
<option value="disabled">выключено</option>
</select>
</label>
<label>
Режим runtime
<select value={workloadForm.runtimeMode} onChange={(event) => setWorkloadForm({ ...workloadForm, runtimeMode: event.target.value })}>
<option value="container">контейнер</option>
<option value="native">нативно</option>
</select>
</label>
<label>
Версия
<input value={workloadForm.version} onChange={(event) => setWorkloadForm({ ...workloadForm, version: event.target.value })} />
</label>
</FormGrid>
<label>
Config JSON
<textarea value={workloadForm.configJson} onChange={(event) => setWorkloadForm({ ...workloadForm, configJson: event.target.value })} />
</label>
<label>
Environment JSON
<textarea
value={workloadForm.environmentJson}
onChange={(event) => setWorkloadForm({ ...workloadForm, environmentJson: event.target.value })}
/>
</label>
<button
className="primary"
disabled={!workloadForm.nodeId || !selectedClusterId}
onClick={() =>
void runAction(
() =>
client.setDesiredWorkload(selectedClusterId, workloadForm.nodeId, workloadForm.serviceType, {
desiredState: workloadForm.desiredState,
runtimeMode: workloadForm.runtimeMode,
version: workloadForm.version,
config: parseJSONObject(workloadForm.configJson, "config сервиса"),
environment: parseJSONObject(workloadForm.environmentJson, "environment сервиса"),
}),
"Желаемое состояние сервиса обновлено.",
)
}
>
Задать желаемое состояние
</button>
</article>
<article className="card">
<h3>Отчеты сервисов</h3>
<div className="stack">
{nodes.map((node) => (
<div className="workloadBlock" key={node.id}>
<strong>{node.name}</strong>
{(workloadsByNode[node.id] || []).length === 0 ? (
<p className="muted">Статус пока не получен.</p>
) : (
<DataTable
columns={["сервис", "состояние", "runtime", "наблюдение"]}
rows={(workloadsByNode[node.id] || []).map((workload) => [
workload.service_type,
workload.reported_state,
workload.runtime_mode,
formatDate(workload.observed_at),
])}
/>
)}
</div>
))}
</div>
</article>
</section>
)}
{activeView === "fabric" && (
<section className="grid two">
<article className="card span2">
<h3>Граница подготовки Fabric</h3>
<p className="muted">
Этот экран показывает synthetic/control-plane подготовку и C17Z11 boundary: production forwarding доступен только для route-bound
`fabric.control` при явном gate. Service traffic, RDP, VPN и произвольный relay здесь не включены.
</p>
<div className="signalStrip">
<Signal label="Synthetic configs" value={`${syntheticConfigEnabledCount}/${nodes.length}`} />
<Signal label="Routes" value={String(syntheticRouteCount)} />
<Signal label="Endpoints / candidates" value={`${syntheticPeerEndpointCount}/${syntheticCandidateCount}`} />
<Signal label="Peer dir / seeds" value={`${syntheticPeerDirectoryCount}/${syntheticRecoverySeedCount}`} />
<Signal label="Scoped production flag" value={productionForwardingConfigCount === 0 ? "false" : `true:${productionForwardingConfigCount}`} />
</div>
</article>
<article className="card">
<div className="cardHead">
<div>
<h3>{t.fabricEntryPoints}</h3>
<p className="muted">{t.fabricEntryPointHelp}</p>
</div>
<span className="pill">{entryPoints.length}</span>
</div>
<FormGrid>
<label>
{t.endpointName}
<input value={entryPointForm.name} onChange={(event) => setEntryPointForm({ ...entryPointForm, name: event.target.value })} />
</label>
<label>
{t.endpointType}
<select
value={entryPointForm.endpointType}
onChange={(event) => setEntryPointForm({ ...entryPointForm, endpointType: event.target.value })}
>
<option value="client_access">client_access</option>
<option value="admin">admin</option>
<option value="api">api</option>
<option value="other">other</option>
</select>
</label>
<label className="span2">
{t.publicEndpoint}
<input
placeholder="wss://entry.example.com"
value={entryPointForm.publicEndpoint}
onChange={(event) => setEntryPointForm({ ...entryPointForm, publicEndpoint: event.target.value })}
/>
</label>
</FormGrid>
<div className="actions">
<button
className="primary"
disabled={!entryPointForm.name.trim()}
onClick={() =>
void runAction(async () => {
await client.createFabricEntryPoint(selectedClusterId, {
name: entryPointForm.name,
endpointType: entryPointForm.endpointType,
publicEndpoint: entryPointForm.publicEndpoint || null,
});
setEntryPointForm({ name: "", endpointType: "client_access", publicEndpoint: "" });
}, "Точка входа создана.")
}
>
{t.createEntryPoint}
</button>
</div>
<div className="stack">
{entryPoints.map((item) => {
const assignedNodes = entryPointNodesById[item.id] || [];
return (
<section className="nodePanel" key={item.id}>
<div className="cardHead">
<div>
<h4>{item.name}</h4>
<p className="muted">
{item.endpoint_type} · {item.public_endpoint || t.addressNotSet}
</p>
</div>
<Status value={item.status} />
</div>
<h5>{t.endpointNodes}</h5>
{assignedNodes.length === 0 ? (
<p className="muted">{t.assignedNodesEmpty}</p>
) : (
<div className="membershipList">
{assignedNodes.map((assignment) => (
<span className={assignment.status === "active" ? "pill good" : "pill"} key={`${item.id}-${assignment.node_id}`}>
{nodeName(nodes, assignment.node_id)} · {statusLabel(assignment.status)} · p{assignment.priority}
</span>
))}
</div>
)}
<div className="actions">
<select
value={entryPointNodeDrafts[item.id] || ""}
onChange={(event) => setEntryPointNodeDrafts({ ...entryPointNodeDrafts, [item.id]: event.target.value })}
>
<option value="">{t.selectNode}</option>
{nodes.map((node) => (
<option key={node.id} value={node.id}>
{node.name}
</option>
))}
</select>
<button
disabled={!entryPointNodeDrafts[item.id]}
onClick={() =>
void runAction(
() => client.setFabricEntryPointNode(selectedClusterId, item.id, entryPointNodeDrafts[item.id], { status: "active" }),
"Узел назначен точке входа.",
)
}
>
{t.assignEndpointNode}
</button>
</div>
</section>
);
})}
{entryPoints.length === 0 && <EmptyState title={t.fabricEntryPoints} text={t.entryPointsEmpty} />}
</div>
</article>
<article className="card">
<div className="cardHead">
<div>
<h3>{t.fabricEgressPools}</h3>
<p className="muted">{t.fabricEgressPoolHelp}</p>
</div>
<span className="pill">{egressPools.length}</span>
</div>
<FormGrid>
<label>
{t.endpointName}
<input value={egressPoolForm.name} onChange={(event) => setEgressPoolForm({ ...egressPoolForm, name: event.target.value })} />
</label>
<label>
{t.description}
<input
value={egressPoolForm.description}
onChange={(event) => setEgressPoolForm({ ...egressPoolForm, description: event.target.value })}
/>
</label>
<label className="span2">
{t.routeScope}
<textarea
rows={5}
value={egressPoolForm.routeScope}
onChange={(event) => setEgressPoolForm({ ...egressPoolForm, routeScope: event.target.value })}
/>
</label>
</FormGrid>
<div className="actions">
<button
className="primary"
disabled={!egressPoolForm.name.trim()}
onClick={() =>
void runAction(async () => {
const routeScope = parseJSONObject(egressPoolForm.routeScope, "Route scope JSON");
await client.createFabricEgressPool(selectedClusterId, {
name: egressPoolForm.name,
description: egressPoolForm.description || null,
routeScope,
});
setEgressPoolForm({ name: "", description: "", routeScope: "{\n \"routes\": []\n}" });
}, "Выходная зона создана.")
}
>
{t.createEgressPool}
</button>
</div>
<div className="stack">
{egressPools.map((item) => {
const assignedNodes = egressPoolNodesById[item.id] || [];
return (
<section className="nodePanel" key={item.id}>
<div className="cardHead">
<div>
<h4>{item.name}</h4>
<p className="muted">{item.description || t.descriptionNotSet}</p>
</div>
<Status value={item.status} />
</div>
<p className="muted">{JSON.stringify(item.route_scope || {})}</p>
<h5>{t.endpointNodes}</h5>
{assignedNodes.length === 0 ? (
<p className="muted">{t.assignedNodesEmpty}</p>
) : (
<div className="membershipList">
{assignedNodes.map((assignment) => (
<span className={assignment.status === "active" ? "pill good" : "pill"} key={`${item.id}-${assignment.node_id}`}>
{nodeName(nodes, assignment.node_id)} · {statusLabel(assignment.status)} · p{assignment.priority}
</span>
))}
</div>
)}
<div className="actions">
<select
value={egressPoolNodeDrafts[item.id] || ""}
onChange={(event) => setEgressPoolNodeDrafts({ ...egressPoolNodeDrafts, [item.id]: event.target.value })}
>
<option value="">{t.selectNode}</option>
{nodes.map((node) => (
<option key={node.id} value={node.id}>
{node.name}
</option>
))}
</select>
<button
disabled={!egressPoolNodeDrafts[item.id]}
onClick={() =>
void runAction(
() => client.setFabricEgressPoolNode(selectedClusterId, item.id, egressPoolNodeDrafts[item.id], { status: "active" }),
"Узел назначен выходной зоне.",
)
}
>
{t.assignEndpointNode}
</button>
</div>
</section>
);
})}
{egressPools.length === 0 && <EmptyState title={t.fabricEgressPools} text={t.egressPoolsEmpty} />}
</div>
</article>
<article className="card span2">
<div className="cardHead">
<div>
<h3>{t.fabricMap}</h3>
<p className="muted">
Визуальный слой показывает, какие узлы живы, какие сервисы на них назначены и какие тестовые наблюдения связей проходят между ними.
</p>
</div>
<Status value={platformTestingFlag?.synthetic_links_enabled ? "enabled" : "disabled"} />
</div>
<FabricTopology
nodes={nodes}
links={meshLinks}
syntheticMeshConfigsByNode={syntheticMeshConfigsByNode}
entryPoints={entryPoints}
entryPointNodesById={entryPointNodesById}
egressPools={egressPools}
egressPoolNodesById={egressPoolNodesById}
rolesByNode={rolesByNode}
workloadsByNode={workloadsByNode}
telemetryByNode={telemetryByNode}
labels={t}
emptyText={t.noLinks}
/>
</article>
<article className="card span2">
<div className="cardHead">
<div>
<h3>Synthetic mesh config</h3>
<p className="muted">
Node-scoped config from Control Plane. Endpoint candidates and scoring inputs are visible to the platform owner only; production
forwarding for service traffic must remain disabled here.
</p>
</div>
<span className={productionForwardingConfigCount === 0 ? "pill good" : "pill bad"}>
production_forwarding={productionForwardingConfigCount === 0 ? "false" : "true"}
</span>
</div>
<DataTable
columns={[
"узел",
"config",
"routes",
"peer endpoints",
"candidates",
"peer dir",
"recovery seeds",
"rendezvous leases",
"relay policy",
"path decisions",
"authority",
"scoped production",
]}
rows={nodes.map((node) => {
const config = syntheticMeshConfigsByNode[node.id];
return [
node.name,
config ? (config.enabled ? "enabled" : "disabled") : "не загружен",
String(config?.routes.length ?? 0),
String(Object.keys(config?.peer_endpoints || {}).length),
String(config ? countPeerEndpointCandidates(config) : 0),
String(config?.peer_directory?.length ?? 0),
String(config?.recovery_seeds?.length ?? 0),
String(config?.rendezvous_leases?.length ?? 0),
rendezvousRelayPolicySummary(config),
routePathDecisionSummary(config),
config?.authority_required ? shortId(config.authority_signature?.key_fingerprint) : "не требуется",
config?.production_forwarding ? "true" : "false",
];
})}
/>
<p className="muted">
Health-aware scoring не выбирает service route и не открывает service-соединения. C17Z19 показывает control-plane route/path
decisions, route generation status, synthetic route-health effective path и relay feedback scoring, но не переносит
RDP/VPN/file/video/service payload.
</p>
</article>
<article className="card span2">
<div className="cardHead">
<div>
<h3>Route intents lifecycle</h3>
<p className="muted">
Operator view for temporary fabric routes. Expired and disabled intents are not emitted into node-scoped synthetic config.
</p>
</div>
<div className="summaryChips">
<span className="pill good">active {routeIntentActive.length}</span>
<span className={routeIntentExpired.length > 0 ? "pill warn" : "pill"}>expired {routeIntentExpired.length}</span>
<span className="pill">disabled {routeIntentDisabled.length}</span>
</div>
</div>
<DataTable
columns={["route", "life", "service", "priority", "source", "destination", "expires", "updated", "actions"]}
rows={routeIntents.slice(0, 120).map((item) => {
const lifecycle = routeIntentLifecycle(item);
return [
shortId(item.id),
<span className={`pill ${routeIntentTone(item)}`}>{lifecycle}</span>,
item.service_class,
String(item.priority),
routeIntentNodeSelector(item.source_selector || {}),
routeIntentNodeSelector(item.destination_selector || {}),
item.policy_expires_at ? formatDate(item.policy_expires_at) : "нет",
formatDate(item.updated_at),
<div className="inlineActions">
{lifecycle === "active" ? (
<button
type="button"
className="ghost"
onClick={() =>
void runAction(
() => client.expireRouteIntent(selectedClusterId, item.id, "operator expired stale route intent"),
"Route intent expired.",
)
}
>
expire
</button>
) : (
<span className="muted">expire</span>
)}
{lifecycle !== "disabled" ? (
<button
type="button"
className="ghost"
onClick={() =>
void runAction(
() => client.disableRouteIntent(selectedClusterId, item.id, "operator disabled route intent"),
"Route intent disabled.",
)
}
>
disable
</button>
) : (
<span className="muted">disable</span>
)}
</div>,
];
})}
/>
{routeIntents.length === 0 && <EmptyState title="Route intents отсутствуют" text="Нет настроенных fabric route intents для текущего кластера." />}
</article>
<article className="card span2">
<div className="cardHead">
<div>
<h3>Service-channel route feedback</h3>
<p className="muted">
Cluster-level runtime feedback from the shared fabric channel. Fenced and no-alternate cases affect route selection for any service class.
</p>
</div>
<div className="summaryChips">
<span className={fabricFeedbackFenced.length > 0 ? "pill bad" : "pill good"}>fenced {fabricFeedbackFenced.length}</span>
<span className={fabricFeedbackDegraded.length > 0 ? "pill warn" : "pill"}>degraded {fabricFeedbackDegraded.length}</span>
<span className={fabricFeedbackRetryCooldown.length > 0 ? "pill warn" : "pill"}>retry {fabricFeedbackRetryCooldown.length}</span>
<span className={fabricFeedbackRecovered.length > 0 ? "pill warn" : "pill"}>recovered {fabricFeedbackRecovered.length}</span>
<span className={fabricFeedbackPromoted.length > 0 ? "pill good" : "pill"}>promoted {fabricFeedbackPromoted.length}</span>
<span className={fabricFeedbackDemoted.length > 0 ? "pill bad" : "pill"}>demoted {fabricFeedbackDemoted.length}</span>
<span className="pill good">healthy {fabricFeedbackHealthy.length}</span>
<span className={fabricNoAlternateDecisions.length > 0 ? "pill bad" : "pill"}>no alternate {fabricNoAlternateDecisions.length}</span>
<span className={fabricRecoveryHysteresisDecisions.length > 0 ? "pill warn" : "pill"}>hysteresis {fabricRecoveryHysteresisDecisions.length}</span>
<span className={fabricRecoveryPromotedDecisions.length > 0 ? "pill good" : "pill"}>promoted paths {fabricRecoveryPromotedDecisions.length}</span>
<span className={fabricRecoveryDemotedDecisions.length > 0 ? "pill bad" : "pill"}>demoted paths {fabricRecoveryDemotedDecisions.length}</span>
<span className={(fabricRecoveryPolicy?.fingerprint || "").length > 0 ? "pill good" : "pill warn"}>policy fp {fabricRecoveryPolicy?.fingerprint ? shortId(fabricRecoveryPolicy.fingerprint) : "нет"}</span>
<span className={fabricRebuildDecisions.length > fabricRebuildAppliedDecisions.length ? "pill warn" : "pill good"}>
rebuild {fabricRebuildAppliedDecisions.length}/{fabricRebuildDecisions.length}
</span>
<span className={fabricRebuildPendingAttempts.length > 0 ? "pill warn" : "pill good"}>
ledger {fabricRebuildAppliedAttempts.length}/{fabricRebuildAttempts.length}
</span>
<span className={fabricRebuildGuardAlerts.length > 0 ? "pill bad" : "pill good"}>guard {fabricRebuildGuardAlerts.length}</span>
<span className={fabricRebuildLedgerDeep ? "pill info" : "pill"}>{fabricRebuildLedgerDeep ? "deep ledger" : "fast ledger"}</span>
</div>
</div>
{fabricNoAlternateDecisions.length > 0 && (
<div className="noticePanel">
Есть service-channel route без unfenced alternate. Для production-сервиса это означает деградацию: fabric не нашел безопасную замену и будет ждать нового маршрута или операторского решения.
</div>
)}
{fabricSchemaStatus && (
<div className={`noticePanel ${fabricSchemaStatus.status === "blocked" ? "badPanel" : "goodPanel"}`}>
<div className="cardHead compact">
<div>
<h4>Fabric schema preflight</h4>
<p className="muted">
Backend/runtime compatibility check for manual deploys before diagnostics or service channels depend on new DB fields.
</p>
</div>
<div className="summaryChips">
<span className={`pill ${fabricSchemaStatus.status === "blocked" ? "bad" : "good"}`}>
{statusLabel(fabricSchemaStatus.status)}
</span>
<span className={fabricSchemaStatus.missing_check_count > 0 ? "pill bad" : "pill good"}>
{fabricSchemaStatus.passed_check_count}/{fabricSchemaStatus.required_check_count}
</span>
<button className="ghost" onClick={() => void warmupFabricRebuildSnapshots()} disabled={loading}>
warm snapshots
</button>
</div>
</div>
<div className="stateList">
<StateLine label="reason" value={statusLabel(fabricSchemaStatus.reason)} />
<StateLine label="required" value={fabricSchemaStatus.required_migration} />
<StateLine label="missing" value={(fabricSchemaStatus.missing_checks || []).map((item) => item.check_id).slice(0, 4).join(", ") || "нет"} />
<StateLine label="action" value={fabricSchemaStatus.recommended_operator_action || "schema is compatible"} />
{fabricSnapshotWarmup && (
<StateLine
label="warmup"
value={`warmed ${fabricSnapshotWarmup.warmed_count}, fresh ${fabricSnapshotWarmup.already_fresh_count}, missing ${fabricSnapshotWarmup.missing_snapshot_count}, stale ${fabricSnapshotWarmup.stale_snapshot_count}, deferred ${fabricSnapshotWarmup.deferred_stale_count}, errors ${fabricSnapshotWarmup.error_count}`}
/>
)}
</div>
</div>
)}
{fabricSnapshotHealth && (
<div className={`noticePanel ${fabricSnapshotHealth.status === "degraded" ? "warnPanel" : "goodPanel"}`}>
<div className="cardHead compact">
<div>
<h4>Snapshot maintenance</h4>
<p className="muted">
Auto-warmup visibility for rebuild snapshot cache after node heartbeats.
</p>
</div>
<div className="summaryChips">
<span className={`pill ${fabricSnapshotHealth.status === "degraded" ? "warn" : "good"}`}>
{statusLabel(fabricSnapshotHealth.status)}
</span>
<span className={fabricSnapshotHealth.overdue_missing_snapshot_count > 0 ? "pill bad" : "pill good"}>
overdue {fabricSnapshotHealth.overdue_missing_snapshot_count}
</span>
<span className={fabricSnapshotHealth.auto_warmup_error_count > 0 ? "pill bad" : "pill good"}>
auto errors {fabricSnapshotHealth.auto_warmup_error_count}
</span>
</div>
</div>
<div className="stateList">
<StateLine label="reason" value={statusLabel(fabricSnapshotHealth.reason)} />
<StateLine label="snapshots" value={`valid ${fabricSnapshotHealth.valid_snapshot_count}, missing ${fabricSnapshotHealth.missing_snapshot_count}, attempts ${fabricSnapshotHealth.recent_attempt_count}`} />
<StateLine label="auto-warmup" value={`events ${fabricSnapshotHealth.auto_warmup_event_count}, warmed ${fabricSnapshotHealth.auto_warmup_warmed_count}, fresh ${fabricSnapshotHealth.auto_warmup_already_fresh_count}, latest ${formatDate(fabricSnapshotHealth.latest_auto_warmup_at)}`} />
<StateLine label="guard" value={`age ${fabricSnapshotHealth.min_age_seconds}s, heartbeats ${fabricSnapshotHealth.heartbeat_threshold}`} />
<StateLine label="action" value={fabricSnapshotHealth.recommended_operator_action || "snapshot maintenance is current"} />
</div>
{(fabricSnapshotHealth.nodes || []).length > 0 && (
<DataTable
columns={["node", "snapshots", "heartbeat", "auto-warmup", "latest"]}
rows={(fabricSnapshotHealth.nodes || []).slice(0, 6).map((item) => [
nodeName(nodes, item.node_id),
<span className={item.overdue_missing_snapshot_count > 0 ? "pill bad" : item.missing_snapshot_count > 0 ? "pill warn" : "pill good"}>
{item.valid_snapshot_count}/{item.recent_attempt_count} overdue {item.overdue_missing_snapshot_count}
</span>,
item.heartbeat_after_attempt_count,
`${item.auto_warmup_warmed_count}/${item.auto_warmup_event_count} errors ${item.auto_warmup_error_count}`,
formatDate(item.latest_auto_warmup_at || item.last_heartbeat_at),
])}
/>
)}
</div>
)}
{fabricLeaseMaintenance && (
<div className={`noticePanel ${fabricLeaseMaintenance.status === "degraded" ? "warnPanel" : "goodPanel"}`}>
<div className="cardHead compact">
<div>
<h4>Service-channel leases</h4>
<p className="muted">
Durable compatibility lease records for introspection after backend restarts.
</p>
</div>
<div className="summaryChips">
<span className={`pill ${fabricLeaseMaintenance.status === "degraded" ? "warn" : "good"}`}>
{statusLabel(fabricLeaseMaintenance.status)}
</span>
<span className="pill good">active {fabricLeaseMaintenance.active_count}</span>
<span className={fabricLeaseMaintenance.expired_count > 0 ? "pill warn" : "pill"}>expired {fabricLeaseMaintenance.expired_count}</span>
<button className="ghost" onClick={() => void cleanupFabricServiceChannelLeases()} disabled={loading}>
cleanup
</button>
</div>
</div>
<div className="stateList">
<StateLine label="reason" value={statusLabel(fabricLeaseMaintenance.reason)} />
<StateLine label="scanned" value={`${fabricLeaseMaintenance.scanned_count}/${fabricLeaseMaintenance.window_limit}`} />
<StateLine label="deleted" value={String(fabricLeaseMaintenance.deleted_expired_count || 0)} />
<StateLine label="action" value={fabricLeaseMaintenance.recommended_operator_action || "lease maintenance is current"} />
</div>
{(fabricLeaseMaintenance.leases || []).length > 0 && (
<DataTable
columns={["expires", "resource", "entry", "exit", "route", "data plane", "state"]}
rows={(fabricLeaseMaintenance.leases || []).slice(0, 8).map((lease) => [
formatDate(lease.expires_at),
lease.resource_id || shortId(lease.channel_id),
nodeName(nodes, lease.selected_entry_node_id || ""),
nodeName(nodes, lease.selected_exit_node_id || ""),
lease.primary_route_id ? `${shortId(lease.primary_route_id)} / ${statusLabel(lease.primary_route_status || "")}` : "backend fallback",
`${statusLabel(lease.data_plane?.working_data_transport || "unknown")} / ${statusLabel(lease.data_plane?.backend_relay_policy || "unknown")}`,
<span className={`pill ${lease.expired ? "warn" : lease.force_backend_fallback ? "warn" : "good"}`}>
{lease.expired ? "expired" : lease.force_backend_fallback ? "fallback" : statusLabel(lease.status)}
</span>,
])}
/>
)}
</div>
)}
{fabricAccessTelemetry && (
<div className={`noticePanel ${fabricAccessTelemetry.status === "degraded" ? "warnPanel" : "goodPanel"}`}>
<div className="cardHead compact">
<div>
<h4>Service-channel access</h4>
<p className="muted">
Live accepted_by visibility from node telemetry and heartbeat metadata.
</p>
</div>
<div className="summaryChips">
<span className={`pill ${fabricAccessTelemetry.status === "degraded" ? "warn" : "good"}`}>
{statusLabel(fabricAccessTelemetry.status)}
</span>
<span className="pill good">accepted {fabricAccessTelemetry.total_accepted}</span>
<span className={fabricAccessTelemetry.backend_fallback_count > 0 ? "pill warn" : "pill"}>
backend {fabricAccessTelemetry.backend_fallback_count}
</span>
<span className={(fabricAccessTelemetry.backend_fallback_blocked_count || 0) > 0 ? "pill bad" : "pill"}>
blocked {fabricAccessTelemetry.backend_fallback_blocked_count || 0}
</span>
<span className={`pill ${fabricAccessTelemetry.last_working_data_transport === "fabric_service_channel" ? "good" : fabricAccessTelemetry.data_plane_contract_count ? "warn" : ""}`}>
data-plane {fabricAccessTelemetry.data_plane_contract_count || 0}
</span>
<span className={`pill ${fabricAccessTelemetry.last_backend_relay_policy === "disabled" ? "good" : fabricAccessTelemetry.last_backend_relay_policy === "degraded_fallback_only" ? "info" : ""}`}>
relay {statusLabel(fabricAccessTelemetry.last_backend_relay_policy || "unknown")}
</span>
<span className={fabricAccessTelemetry.degraded_fallback_channel_count > 0 || fabricAccessTelemetry.degraded_route_count > 0 ? "pill warn" : "pill good"}>
channels {fabricAccessTelemetry.active_channel_count}
</span>
<span className={fabricAccessTelemetry.no_safe_recovery_decision_count ? "pill warn" : fabricAccessTelemetry.route_decision_channel_count ? "pill info" : "pill"}>
decisions {fabricAccessTelemetry.route_decision_channel_count || 0}
{fabricAccessTelemetry.replacement_decision_count ? ` / repl ${fabricAccessTelemetry.replacement_decision_count}` : ""}
{fabricAccessTelemetry.applied_rebuild_decision_count ? ` / applied ${fabricAccessTelemetry.applied_rebuild_decision_count}` : ""}
{fabricAccessTelemetry.recovery_decision_count ? ` / recovery ${fabricAccessTelemetry.recovery_decision_count}` : ""}
{fabricAccessTelemetry.no_safe_recovery_decision_count ? ` / no-safe ${fabricAccessTelemetry.no_safe_recovery_decision_count}` : ""}
</span>
<span className={`pill ${flowHealthPillClass(fabricAccessTelemetry.flow_health_status, fabricAccessTelemetry.flow_dropped)}`}>
{formatTrafficClassCounts(fabricAccessTelemetry.traffic_class_counts)}
</span>
<span className={`pill ${flowHealthPillClass(fabricAccessTelemetry.flow_health_status, fabricAccessTelemetry.flow_dropped)}`}>
flow {statusLabel(fabricAccessTelemetry.flow_health_status || "healthy")}
</span>
<span className={`pill ${fabricAccessTelemetry.adaptive_backpressure_active ? "info" : "good"}`}>
windows {formatRecommendedWindows(fabricAccessTelemetry.recommended_parallel_windows)}
</span>
</div>
</div>
<div className="stateList">
<StateLine label="reason" value={statusLabel(fabricAccessTelemetry.reason)} />
<StateLine label="reporting nodes" value={`${fabricAccessTelemetry.reporting_node_count}/${fabricAccessTelemetry.node_count}`} />
<StateLine label="accepted by" value={`signed ${fabricAccessTelemetry.signed_accepted}, introspection ${fabricAccessTelemetry.introspection_accepted}, legacy ${fabricAccessTelemetry.legacy_unsigned_accepted}`} />
<StateLine label="data plane" value={`${fabricAccessTelemetry.data_plane_contract_count || 0} contracts, mode ${statusLabel(fabricAccessTelemetry.last_data_plane_mode || "unknown")}, working ${statusLabel(fabricAccessTelemetry.last_working_data_transport || "unknown")}, steady ${statusLabel(fabricAccessTelemetry.last_steady_state_transport || "unknown")}, relay ${statusLabel(fabricAccessTelemetry.last_backend_relay_policy || "unknown")}, flows ${statusLabel(fabricAccessTelemetry.last_logical_flow_mode || "unknown")}, blocked ${fabricAccessTelemetry.backend_fallback_blocked_count || 0}, route failures ${fabricAccessTelemetry.fabric_route_send_failure_count || 0}`} />
<StateLine label="data-plane violation" value={fabricAccessTelemetry.last_data_plane_violation_status ? `${statusLabel(fabricAccessTelemetry.last_data_plane_violation_status)} / ${fabricAccessTelemetry.last_data_plane_violation_reason || "n/a"}` : "none"} />
<StateLine label="active channels" value={`${fabricAccessTelemetry.active_channel_count || 0}, fallback ${fabricAccessTelemetry.degraded_fallback_channel_count || 0}, correlated routes ${fabricAccessTelemetry.correlated_route_count || 0}, degraded routes ${fabricAccessTelemetry.degraded_route_count || 0}`} />
<StateLine label="route decisions" value={`channels ${fabricAccessTelemetry.route_decision_channel_count || 0}, replacement ${fabricAccessTelemetry.replacement_decision_count || 0}, applied ${fabricAccessTelemetry.applied_rebuild_decision_count || 0}, recovery ${fabricAccessTelemetry.recovery_decision_count || 0}, no-safe ${fabricAccessTelemetry.no_safe_recovery_decision_count || 0}`} />
<StateLine label="flow QoS" value={`${statusLabel(fabricAccessTelemetry.flow_health_status || "healthy")} / ${statusLabel(fabricAccessTelemetry.flow_health_reason || "flow_health_ready")}, ${formatTrafficClassCounts(fabricAccessTelemetry.traffic_class_counts)}, flows ${fabricAccessTelemetry.flow_channel_count || 0}, in-flight ${fabricAccessTelemetry.flow_max_in_flight || 0}, dropped ${fabricAccessTelemetry.flow_dropped || 0}`} />
<StateLine label="adaptive windows" value={`${fabricAccessTelemetry.adaptive_backpressure_active ? statusLabel(fabricAccessTelemetry.adaptive_backpressure_reason || "adaptive") : "off"}, ${formatRecommendedWindows(fabricAccessTelemetry.recommended_parallel_windows)}, policy ${fabricAccessTelemetry.adaptive_policy_fingerprint ? shortId(fabricAccessTelemetry.adaptive_policy_fingerprint) : "n/a"}`} />
<StateLine label="latest accepted" value={formatDate(fabricAccessTelemetry.latest_accepted_at)} />
<StateLine label="action" value={fabricAccessTelemetry.recommended_operator_action || "access telemetry is current"} />
</div>
{(fabricAccessTelemetry.active_channels || []).length > 0 && (
<DataTable
columns={["resource", "entry -> exit", "route", "decision", "entry access", "data plane", "flow health", "windows", "flow QoS", "route quality", "remediation", "guard", "execution", "expires"]}
rows={(fabricAccessTelemetry.active_channels || []).slice(0, 10).map((item) => [
item.resource_id || shortId(item.channel_id),
`${nodeName(nodes, item.selected_entry_node_id || "")} -> ${nodeName(nodes, item.selected_exit_node_id || "")}`,
item.primary_route_id ? `${shortId(item.primary_route_id)} / ${statusLabel(item.primary_route_status || "")}` : "backend fallback",
<span className={`pill ${routeDecisionTone(item.route_decision_source, item.route_decision_rebuild_status, item.route_decision_score_reasons)}`}>
{item.route_decision_source
? `${statusLabel(item.route_decision_source)}${item.route_decision_route_id ? ` ${shortId(item.route_decision_route_id)}` : ""}${item.route_decision_replacement_route_id ? ` -> ${shortId(item.route_decision_replacement_route_id)}` : ""}${item.route_decision_rebuild_status ? ` / ${statusLabel(item.route_decision_rebuild_status)}` : ""}`
: "n/a"}
</span>,
`accepted ${item.entry_node_total_accepted}, introspection ${item.entry_node_introspection_accepted}, backend ${item.entry_node_backend_fallback_count}`,
<span className={`pill ${item.entry_node_last_working_data_transport === "fabric_service_channel" ? "good" : item.entry_node_data_plane_contract_count ? "warn" : ""}`}>
{`${item.entry_node_data_plane_contract_count || 0} / ${statusLabel(item.entry_node_last_working_data_transport || "unknown")} / ${statusLabel(item.entry_node_last_backend_relay_policy || "unknown")}${item.entry_node_backend_fallback_blocked_count ? ` / blocked ${item.entry_node_backend_fallback_blocked_count}` : ""}`}
</span>,
<span className={`pill ${flowHealthPillClass(item.entry_node_flow_health_status, item.entry_node_flow_dropped)}`}>
{statusLabel(item.entry_node_flow_health_status || "healthy")}
{item.entry_node_flow_health_reason ? ` / ${statusLabel(item.entry_node_flow_health_reason)}` : ""}
</span>,
<span className={`pill ${item.entry_node_adaptive_backpressure_active ? "info" : "good"}`}>
{formatRecommendedWindows(item.entry_node_recommended_parallel_windows)}
</span>,
<span className={`pill ${flowHealthPillClass(item.entry_node_flow_health_status, item.entry_node_flow_dropped)}`}>
{formatTrafficClassCounts(item.entry_node_traffic_class_counts)}
{` / flows ${item.entry_node_flow_channel_count || 0} / in ${item.entry_node_flow_max_in_flight || 0}`}
</span>,
<span className={`pill ${item.force_backend_fallback || item.route_feedback_status === "degraded" || item.route_feedback_status === "fenced" ? "warn" : item.route_feedback_status ? "good" : ""}`}>
{item.force_backend_fallback
? "backend fallback"
: item.route_feedback_status
? `${statusLabel(item.route_feedback_status)} / ${item.last_send_duration_ms || 0}ms / q ${item.route_quality_window_sample_count || 0}`
: "no route feedback"}
</span>,
<span className={`pill ${item.remediation_action === "none" ? "good" : item.remediation_action === "prefer_alternate_route" ? "warn" : item.remediation_action ? "bad" : ""}`}>
{item.remediation_action
? `${item.remediation_command ? "cmd " : ""}${statusLabel(item.remediation_action)}${item.remediation_command?.replacement_route_id ? ` -> ${shortId(item.remediation_command.replacement_route_id)}` : item.remediation_route_id ? ` -> ${shortId(item.remediation_route_id)}` : ""}`
: "n/a"}
</span>,
<span className={`pill ${item.remediation_guard_status === "rejected" ? "bad" : item.pool_policy_fingerprint ? "good" : ""}`}>
{item.remediation_guard_status ? statusLabel(item.remediation_guard_status) : item.pool_policy_fingerprint ? "pool policy" : "n/a"}
{item.remediation_guard_reason ? ` / ${statusLabel(item.remediation_guard_reason)}` : ""}
</span>,
<span className={`pill ${remediationExecutionTone(item.remediation_execution_status)}`}>
{item.remediation_execution_status ? statusLabel(item.remediation_execution_status) : "n/a"}
{item.remediation_execution_generation ? ` / ${shortId(item.remediation_execution_generation)}` : ""}
{item.remediation_execution_reason ? ` / ${statusLabel(item.remediation_execution_reason)}` : ""}
</span>,
formatDate(item.expires_at),
])}
/>
)}
{(fabricAccessTelemetry.nodes || []).length > 0 && (
<DataTable
columns={["node", "accepted", "signed", "introspection", "legacy", "backend", "data plane", "flow health", "windows", "flow QoS", "latest"]}
rows={(fabricAccessTelemetry.nodes || []).slice(0, 10).map((item) => [
nodeName(nodes, item.node_id) || item.node_name || shortId(item.node_id),
item.total_accepted,
item.signed_accepted,
item.introspection_accepted,
item.legacy_unsigned_accepted,
<span className={item.backend_fallback_count > 0 ? "pill warn" : "pill"}>
{item.backend_fallback_count}
</span>,
<span className={`pill ${item.last_working_data_transport === "fabric_service_channel" ? "good" : item.data_plane_contract_count ? "warn" : ""}`}>
{`${item.data_plane_contract_count || 0} / ${statusLabel(item.last_working_data_transport || "unknown")} / ${statusLabel(item.last_backend_relay_policy || "unknown")}${item.backend_fallback_blocked_count ? ` / blocked ${item.backend_fallback_blocked_count}` : ""}`}
</span>,
<span className={`pill ${flowHealthPillClass(item.flow_health_status, item.flow_dropped)}`}>
{statusLabel(item.flow_health_status || "healthy")}
{item.flow_health_reason ? ` / ${statusLabel(item.flow_health_reason)}` : ""}
</span>,
<span className={`pill ${item.adaptive_backpressure_active ? "info" : "good"}`}>
{formatRecommendedWindows(item.recommended_parallel_windows)}
</span>,
<span className={`pill ${flowHealthPillClass(item.flow_health_status, item.flow_dropped)}`}>
{formatTrafficClassCounts(item.traffic_class_counts)}
{` / flows ${item.flow_channel_count || 0} / in ${item.flow_max_in_flight || 0}`}
</span>,
formatDate(item.last_accepted_at || item.observed_at),
])}
/>
)}
</div>
)}
{fabricReadiness && (
<div className={`noticePanel ${fabricReadiness.status === "blocked" ? "badPanel" : fabricReadiness.status === "degraded" ? "warnPanel" : "goodPanel"}`}>
<div className="cardHead compact">
<div>
<h4>Fabric service-channel readiness</h4>
<p className="muted">
Verdict for production service-channel use. Working service payloads should not depend on this fabric while the gate is blocked.
</p>
</div>
<div className="summaryChips">
<span className={`pill ${fabricReadiness.status === "blocked" ? "bad" : fabricReadiness.status === "degraded" ? "warn" : "good"}`}>
{statusLabel(fabricReadiness.status)}
</span>
<span className={fabricReadiness.active_alert_count > 0 ? "pill bad" : "pill"}>active {fabricReadiness.active_alert_count}</span>
<span className={fabricReadiness.resurfaced_count > 0 ? "pill bad" : "pill"}>resurfaced {fabricReadiness.resurfaced_count}</span>
</div>
</div>
<div className="stateList">
<StateLine label="reason" value={statusLabel(fabricReadiness.reason)} />
<StateLine label="blocking" value={(fabricReadiness.blocking_reasons || []).map(statusLabel).join(", ") || "нет"} />
<StateLine label="degraded" value={(fabricReadiness.degraded_reasons || []).map(statusLabel).join(", ") || "нет"} />
<StateLine label="missing/post" value={`transition ${fabricReadiness.missing_transition_count}, route-gen ${fabricReadiness.missing_route_generation_count}, traffic ${fabricReadiness.missing_post_rebuild_traffic_count}`} />
</div>
</div>
)}
{fabricRebuildIncidents.length > 0 && (
<div className="subPanel">
<div className="cardHead compact">
<div>
<h4>Rebuild incidents</h4>
<p className="muted">
Grouped recent rebuild attempts by reporter, route, service, generation, and guard. Open an incident to load the exact deep ledger slice.
</p>
</div>
<span className="pill">{fabricRebuildIncidents.length}</span>
</div>
<DataTable
columns={["last", "source", "reporter", "route", "service", "guard", "count", "replacement", "action"]}
rows={fabricRebuildIncidents.slice(0, 10).map((incident) => [
formatDate(incident.last_seen_at),
incident.incident_source ? statusLabel(incident.incident_source) : "ledger",
nodeName(nodes, incident.reporter_node_id),
shortId(incident.route_id),
incident.service_class,
<span className={`pill ${incident.alert_resurfaced ? "bad" : incident.guard_severity === "bad" ? "bad" : incident.guard_severity === "warn" ? "warn" : "good"}`}>
{statusLabel(incident.guard_status)}{incident.alert_silenced ? " / silenced" : incident.alert_resurfaced ? " / resurfaced" : ""}
</span>,
String(incident.attempt_count),
incident.latest_replacement_route_id ? shortId(incident.latest_replacement_route_id) : "нет",
<div className="inlineActions">
<span>
{statusLabel(incident.recommended_operator_action || "inspect")}
{incident.alert_resurfaced && incident.alert_resurfaced_cause
? ` (${statusLabel(incident.alert_resurfaced_cause)})`
: ""}
{incident.alert_resurfaced && incident.alert_resurfaced_previous_generation
? ` from ${shortId(incident.alert_resurfaced_previous_generation)} until ${formatDate(incident.alert_resurfaced_previous_until)}`
: ""}
</span>
<button
type="button"
className="ghost"
onClick={() => void runAction(() => openFabricRebuildIncidentDeepLedger(incident), "Deep rebuild investigation opened.")}
>
open deep
</button>
{!incident.alert_silenced ? (
<button
type="button"
className="ghost"
onClick={() => void runAction(() => silenceFabricRebuildIncident(incident), "Rebuild incident silenced for 6 hours.")}
>
silence 6h
</button>
) : (
<span className="muted">silenced</span>
)}
</div>,
])}
/>
</div>
)}
{(fabricBreadcrumbWindowPolicy || fabricDrilldownAuditEvents.length > 0) && (
<div className="subPanel">
<div className="cardHead compact">
<div>
<h4>Recent investigations</h4>
<p className="muted">
Recent operator drilldowns opened from rebuild incidents or feedback breakdown rows.
</p>
</div>
<div className="summaryChips">
<span className="pill info">{fabricDrilldownAuditSummary?.total_count || fabricDrilldownAuditEvents.length}</span>
<span className="pill good">linked {fabricDrilldownAuditSummary?.correlated_count || 0}</span>
<span className={(fabricDrilldownAuditSummary?.not_visible_count || 0) > 0 ? "pill warn" : "pill"}>
not visible {fabricDrilldownAuditSummary?.not_visible_count || 0}
</span>
{Object.entries(fabricDrilldownAuditSummary?.counts_by_breadcrumb_status || {}).map(([status, count]) => (
<span key={status} className={status === "current" ? "pill good" : status === "stale" ? "pill warn" : "pill bad"}>
{statusLabel(status)} {count}
</span>
))}
{Object.entries(fabricDrilldownAuditSummary?.counts_by_current_diagnostic_status || {}).slice(0, 3).map(([status, count]) => (
<span key={status} className={status === "breakdown_active" || status === "incident_visible" ? "pill good" : status === "not_visible" ? "pill warn" : "pill"}>
{statusLabel(status)} {count}
</span>
))}
</div>
</div>
{fabricBreadcrumbWindowPolicy && (
<div className="inlineForm">
<label>
current window, sec
<input
type="number"
min="60"
value={fabricBreadcrumbWindowPolicyForm.currentWindowSeconds}
onChange={(event) => setFabricBreadcrumbWindowPolicyForm((form) => ({ ...form, currentWindowSeconds: event.target.value }))}
/>
</label>
<label>
history window, sec
<input
type="number"
min="60"
value={fabricBreadcrumbWindowPolicyForm.historyWindowSeconds}
onChange={(event) => setFabricBreadcrumbWindowPolicyForm((form) => ({ ...form, historyWindowSeconds: event.target.value }))}
/>
</label>
<button
type="button"
className="ghost"
onClick={() =>
void runAction(async () => {
const updated = await client.updateFabricServiceChannelBreadcrumbWindowPolicy(selectedClusterId, {
currentWindowSeconds: Number(fabricBreadcrumbWindowPolicyForm.currentWindowSeconds),
historyWindowSeconds: Number(fabricBreadcrumbWindowPolicyForm.historyWindowSeconds),
});
setFabricBreadcrumbWindowPolicy(updated);
const drilldownAudit = await client.getFabricServiceChannelRebuildInvestigationBreadcrumbs(selectedClusterId, { limit: 20 });
setFabricDrilldownAudit(drilldownAudit.events);
setFabricDrilldownAuditSummary(drilldownAudit.summary || null);
}, "Breadcrumb window policy updated.")
}
>
apply windows
</button>
<span className="muted">
source {fabricBreadcrumbWindowPolicy.source}, fp {shortId(fabricBreadcrumbWindowPolicy.fingerprint || "")}
</span>
</div>
)}
{fabricDrilldownAuditSummary && (
<div className="stateList">
<StateLine label="latest" value={formatDate(fabricDrilldownAuditSummary.latest_at)} />
<StateLine
label="windows"
value={`${fabricBreadcrumbWindowPolicy?.current_window_seconds || "n/a"}s current / ${fabricBreadcrumbWindowPolicy?.history_window_seconds || "n/a"}s history`}
/>
<StateLine
label="sources"
value={
Object.entries(fabricDrilldownAuditSummary.counts_by_feedback_source || {})
.slice(0, 3)
.map(([source, count]) => `${statusLabel(source)} ${count}`)
.join(", ") || "нет"
}
/>
<StateLine
label="violations"
value={
Object.entries(fabricDrilldownAuditSummary.counts_by_feedback_violation_status || {})
.slice(0, 3)
.map(([status, count]) => `${statusLabel(status)} ${count}`)
.join(", ") || "нет"
}
/>
</div>
)}
<DataTable
columns={["time", "freshness", "source", "feedback", "target", "current", "actor", "reason"]}
rows={fabricDrilldownAuditEvents.map((event) => {
const payload = objectField(event.payload) || {};
const feedbackChannelID = stringField(payload, "feedback_channel_id", "");
const feedbackViolationStatus = stringField(payload, "feedback_violation_status", "");
const feedbackSource = stringField(payload, "feedback_source", "");
const reporterNodeID = stringField(payload, "reporter_node_id", "");
const routeID = stringField(payload, "route_id", "");
const drilldownSource = stringField(payload, "drilldown_source", "");
const hintStatus = event.correlation_hints?.current_diagnostic_status || "";
const breadcrumbStatus = event.correlation_hints?.breadcrumb_status || "current";
const breadcrumbAgeSeconds = event.correlation_hints?.breadcrumb_age_seconds;
const matchingBreakdown = event.correlation_hints?.feedback_breakdown || fabricFeedbackBreakdownForAuditEvent(event);
const matchingIncident = event.correlation_hints?.rebuild_incident || fabricRebuildIncidentForAuditEvent(event);
return [
formatDate(event.created_at),
<div className="stackedText">
<span className={breadcrumbStatus === "current" ? "pill good" : breadcrumbStatus === "stale" ? "pill warn" : "pill bad"}>
{statusLabel(breadcrumbStatus)}
</span>
<span className="muted">{formatAgeSeconds(breadcrumbAgeSeconds)}</span>
</div>,
<div className="stackedText">
<span>{event.event_type.includes("feedback_breakdown") ? "feedback breakdown" : "incident"}</span>
<span className="muted">{statusLabel(drilldownSource || event.target_type)}</span>
</div>,
feedbackSource || feedbackChannelID || feedbackViolationStatus ? (
<div className="stackedText">
<span>{statusLabel(feedbackSource || "feedback")}</span>
<span className="muted">{feedbackChannelID ? `ch ${shortId(feedbackChannelID)}` : "any channel"}</span>
<span className="muted">{statusLabel(feedbackViolationStatus || "any violation")}</span>
</div>
) : (
<span className="muted">нет</span>
),
<div className="stackedText">
<span>{reporterNodeID ? nodeName(nodes, reporterNodeID) : "any reporter"}</span>
<span className="muted">{routeID ? shortId(routeID) : event.target_id ? shortId(event.target_id) : "any route"}</span>
</div>,
matchingBreakdown ? (
<div className="inlineActions">
<span className={matchingBreakdown.active_bad_count ? "pill bad" : matchingBreakdown.active_warn_count ? "pill warn" : "pill good"}>
{statusLabel(hintStatus || "breakdown_active")}
</span>
<span className="muted">
bad {matchingBreakdown.active_bad_count || 0} / warn {matchingBreakdown.active_warn_count || 0}
</span>
<button
type="button"
className="ghost"
onClick={() => void runAction(() => openFabricRebuildFeedbackBreakdownLedger(matchingBreakdown), "Rebuild ledger opened for current feedback breakdown.")}
>
open
</button>
</div>
) : matchingIncident ? (
<div className="inlineActions">
<span className={`pill ${matchingIncident.guard_severity === "bad" ? "bad" : matchingIncident.guard_severity === "warn" ? "warn" : "good"}`}>
{statusLabel(hintStatus || "incident_visible")}
</span>
<span className="muted">{statusLabel(matchingIncident.guard_status)}</span>
<button
type="button"
className="ghost"
onClick={() => void runAction(() => openFabricRebuildIncidentDeepLedger(matchingIncident), "Deep rebuild investigation opened for current incident.")}
>
open
</button>
</div>
) : (
<span className="muted">{statusLabel(hintStatus || "not_visible")}</span>
),
event.actor_user_id ? shortId(event.actor_user_id) : "system",
stringField(payload, "reason", "operator opened investigation"),
];
})}
/>
</div>
)}
{fabricRebuildSilences.length > 0 && (
<div className="subPanel">
<div className="cardHead compact">
<div>
<h4>Active rebuild silences</h4>
<p className="muted">
Operator acknowledgements currently suppressing rebuild/access-decision alerts. Remove a silence to let the incident become active again.
</p>
</div>
<span className="pill info">{fabricRebuildSilences.length}</span>
</div>
<DataTable
columns={["until", "source", "channel", "reporter", "route", "guard", "reason", "action"]}
rows={fabricRebuildSilences.slice(0, 10).map((silence) => [
formatDate(silence.expires_at),
silence.incident_source ? statusLabel(silence.incident_source) : "ledger",
silence.channel_id ? shortId(silence.channel_id) : "нет",
nodeName(nodes, silence.reporter_node_id),
shortId(silence.display_route_id || silence.route_id),
statusLabel(silence.guard_status),
silence.reason || "acknowledged",
<button
type="button"
className="ghost"
onClick={() => void runAction(() => unsilenceFabricRebuildAlert(silence), "Rebuild alert silence removed.")}
>
unsilence
</button>,
])}
/>
</div>
)}
{fabricRebuildHealth && (
<div className="subPanel">
<div className="cardHead compact">
<div>
<h4>Rebuild health</h4>
<p className="muted">
Сводка по последним {fabricRebuildHealth.total_attempts} rebuild попыткам. Данные помогают быстро увидеть, где backend уже принял решение, но node-agent или post-rebuild traffic не подтвердили результат.
</p>
</div>
<div className="summaryChips">
<span className="pill good">ok {fabricRebuildHealth.good_count}</span>
<span className={fabricRebuildHealth.active_warn_count > 0 ? "pill warn" : "pill"}>warn {fabricRebuildHealth.active_warn_count}/{fabricRebuildHealth.warn_count}</span>
<span className={fabricRebuildHealth.active_bad_count > 0 ? "pill bad" : "pill"}>bad {fabricRebuildHealth.active_bad_count}/{fabricRebuildHealth.bad_count}</span>
<span className={fabricRebuildHealth.resurfaced_count > 0 ? "pill bad" : "pill"}>resurfaced {fabricRebuildHealth.resurfaced_count}</span>
<span className={fabricRebuildHealth.silenced_count > 0 ? "pill info" : "pill"}>silenced {fabricRebuildHealth.silenced_count}</span>
<span className="pill">applied {fabricRebuildHealth.applied_count}</span>
<span className={fabricRebuildHealth.access_no_safe_count ? "pill bad" : fabricRebuildHealth.access_route_decision_count ? "pill info" : "pill"}>
access {fabricRebuildHealth.access_route_decision_count || 0}
{fabricRebuildHealth.access_no_safe_count ? ` / no-safe ${fabricRebuildHealth.access_no_safe_count}` : ""}
</span>
</div>
</div>
<div className="stateList">
<StateLine label="observed" value={formatDate(fabricRebuildHealth.observed_at)} />
<StateLine label="affected nodes" value={(fabricRebuildHealth.affected_reporter_node_ids || []).map((id) => nodeName(nodes, id)).join(", ") || "нет"} />
<StateLine label="affected routes" value={(fabricRebuildHealth.affected_route_ids || []).map(shortId).join(", ") || "нет"} />
<StateLine label="action" value={statusLabel(fabricRebuildHealth.recommended_operator_action || "no_operator_action_required")} />
</div>
{(fabricRebuildHealth.feedback_breakdowns || []).length > 0 && (
<DataTable
columns={["feedback", "active", "total", "affected", "incidents", "latest", "action"]}
rows={(fabricRebuildHealth.feedback_breakdowns || []).slice(0, 8).map((item) => {
const relatedIncidents = fabricRebuildIncidentsForFeedbackBreakdown(item);
return [
<div className="stackedText">
<span>{statusLabel(item.feedback_source || "feedback")}</span>
<span className="muted">{item.feedback_channel_id ? `ch ${shortId(item.feedback_channel_id)}` : "any channel"}</span>
<span className="muted">{statusLabel(item.feedback_violation_status || "unknown")}</span>
</div>,
<span className={item.active_bad_count ? "pill bad" : item.active_warn_count ? "pill warn" : "pill"}>
bad {item.active_bad_count || 0} / warn {item.active_warn_count || 0}
</span>,
`total ${item.total_count} / bad ${item.bad_count || 0} / warn ${item.warn_count || 0} / silenced ${item.silenced_count || 0}`,
<div className="stackedText">
<span>{(item.affected_reporter_node_ids || []).map((id) => nodeName(nodes, id)).join(", ") || "нет узлов"}</span>
<span className="muted">{(item.affected_route_ids || []).map(shortId).join(", ") || "нет routes"}</span>
</div>,
relatedIncidents.length > 0 ? (
<div className="stackedText">
<span className="pill warn">{relatedIncidents.length}</span>
<span className="muted">{relatedIncidents.slice(0, 2).map((incident) => statusLabel(incident.guard_status)).join(", ")}</span>
</div>
) : (
<span className="muted">нет</span>
),
formatDate(item.latest_observed_at),
<button
type="button"
className="ghost"
onClick={() => void runAction(() => openFabricRebuildFeedbackBreakdownLedger(item), "Rebuild ledger opened for feedback breakdown.")}
>
open ledger
</button>,
];
})}
/>
)}
{(fabricRebuildHealth.most_recent_bad_attempts || []).length > 0 && (
<DataTable
columns={["time", "reporter", "route", "guard", "reason"]}
rows={(fabricRebuildHealth.most_recent_bad_attempts || []).slice(0, 5).map((attempt) => [
formatDate(attempt.updated_at),
nodeName(nodes, attempt.reporter_node_id),
shortId(attempt.route_id),
<span className="pill bad">{statusLabel(attempt.guard_status || "bad")}</span>,
<div className="inlineActions">
<span>{statusLabel(attempt.guard_reason || "unknown")}</span>
<button
type="button"
className="ghost"
onClick={() =>
void runAction(
() =>
client.silenceFabricServiceChannelRouteRebuildAlert(selectedClusterId, {
reporterNodeId: attempt.reporter_node_id,
routeId: attempt.route_id,
guardStatus: attempt.guard_status || "unknown",
generation: attempt.generation || "",
reason: "operator acknowledged known rebuild alert",
ttlSeconds: 21600,
}),
"Rebuild alert silenced for this route generation.",
)
}
>
silence 6h
</button>
</div>,
])}
/>
)}
{(fabricRebuildHealth.resurfaced_attempts || []).length > 0 && (
<DataTable
columns={["time", "reporter", "route", "guard", "previous", "action"]}
rows={(fabricRebuildHealth.resurfaced_attempts || []).slice(0, 5).map((attempt) => [
formatDate(attempt.updated_at),
nodeName(nodes, attempt.reporter_node_id),
shortId(attempt.route_id),
<span className="pill bad">{statusLabel(attempt.guard_status || "bad")}</span>,
`${shortId(attempt.alert_resurfaced_previous_generation)} until ${formatDate(attempt.alert_resurfaced_previous_until)}`,
<div className="inlineActions">
<span>{statusLabel(attempt.guard_reason || "unknown")}</span>
<button
type="button"
className="ghost"
onClick={() =>
void runAction(
() =>
client.silenceFabricServiceChannelRouteRebuildAlert(selectedClusterId, {
reporterNodeId: attempt.reporter_node_id,
routeId: attempt.route_id,
guardStatus: attempt.guard_status || "unknown",
generation: attempt.generation || "",
reason: "operator acknowledged resurfaced rebuild alert",
ttlSeconds: 21600,
}),
"Resurfaced rebuild alert silenced for this generation.",
)
}
>
silence 6h
</button>
</div>,
])}
/>
)}
</div>
)}
{fabricRecoveryPolicy && (
<div className="inlineForm">
<label>
penalty
<input
type="number"
min="0"
value={fabricRecoveryPolicyForm.hysteresisPenalty}
onChange={(event) => setFabricRecoveryPolicyForm((form) => ({ ...form, hysteresisPenalty: event.target.value }))}
/>
</label>
<label>
promote samples
<input
type="number"
min="1"
value={fabricRecoveryPolicyForm.promotionMinSamples}
onChange={(event) => setFabricRecoveryPolicyForm((form) => ({ ...form, promotionMinSamples: event.target.value }))}
/>
</label>
<label>
fail
<input
type="number"
min="1"
value={fabricRecoveryPolicyForm.demotionFailureThreshold}
onChange={(event) => setFabricRecoveryPolicyForm((form) => ({ ...form, demotionFailureThreshold: event.target.value }))}
/>
</label>
<label>
drop
<input
type="number"
min="1"
value={fabricRecoveryPolicyForm.demotionDropThreshold}
onChange={(event) => setFabricRecoveryPolicyForm((form) => ({ ...form, demotionDropThreshold: event.target.value }))}
/>
</label>
<label>
slow
<input
type="number"
min="1"
value={fabricRecoveryPolicyForm.demotionSlowThreshold}
onChange={(event) => setFabricRecoveryPolicyForm((form) => ({ ...form, demotionSlowThreshold: event.target.value }))}
/>
</label>
<label className="checkLine">
<input
type="checkbox"
checked={fabricRecoveryPolicyForm.demotionRebuildEnabled}
onChange={(event) => setFabricRecoveryPolicyForm((form) => ({ ...form, demotionRebuildEnabled: event.target.checked }))}
/>
rebuild
</label>
<label className="checkLine">
<input
type="checkbox"
checked={fabricRecoveryPolicyForm.demotionFencedEnabled}
onChange={(event) => setFabricRecoveryPolicyForm((form) => ({ ...form, demotionFencedEnabled: event.target.checked }))}
/>
fenced
</label>
<button
type="button"
className="ghost"
onClick={() =>
void runAction(async () => {
const updated = await client.updateFabricServiceChannelRecoveryPolicy(selectedClusterId, {
hysteresisPenalty: Number(fabricRecoveryPolicyForm.hysteresisPenalty),
promotionMinSamples: Number(fabricRecoveryPolicyForm.promotionMinSamples),
demotionFailureThreshold: Number(fabricRecoveryPolicyForm.demotionFailureThreshold),
demotionDropThreshold: Number(fabricRecoveryPolicyForm.demotionDropThreshold),
demotionSlowThreshold: Number(fabricRecoveryPolicyForm.demotionSlowThreshold),
demotionRebuildEnabled: fabricRecoveryPolicyForm.demotionRebuildEnabled,
demotionFencedEnabled: fabricRecoveryPolicyForm.demotionFencedEnabled,
});
setFabricRecoveryPolicy(updated);
}, "Recovery policy updated.")
}
>
apply policy
</button>
<span className="muted">source {fabricRecoveryPolicy.source}</span>
</div>
)}
<DataTable
columns={["route", "reporter", "service", "status", "recovery", "score", "reasons", "failures", "retry/cooldown", "expires", "action"]}
rows={fabricRouteFeedbackVisible.slice(0, 80).map((item) => [
shortId(item.route_id),
nodeName(nodes, item.reporter_node_id),
item.service_class,
<span className={`pill ${serviceChannelFeedbackTone(item.feedback_status)}`}>{statusLabel(item.feedback_status)}</span>,
item.recovery_state ? (
<span className={`pill ${serviceChannelRecoveryTone(item.recovery_state)}`}>
{item.recovery_demoted ? `demoted ${item.recovery_reason ? statusLabel(item.recovery_reason) : ""}` : item.recovery_promoted ? "promoted" : statusLabel(item.recovery_state)}
{item.recovery_hysteresis_penalty ? ` -${item.recovery_hysteresis_penalty}` : ""}
</span>
) : item.stale_policy || item.stale_generation ? (
<span className="pill warn">{statusLabel(item.stale_reason || "stale")}</span>
) : item.provenance_missing ? (
<span className="pill warn">provenance missing</span>
) : (
"нет"
),
String(item.score_adjustment),
(item.reasons || []).join(", ") || "нет",
String(item.consecutive_failures || 0),
item.retry_cooldown_until ? formatDate(item.retry_cooldown_until) : "нет",
formatDate(item.expires_at),
item.feedback_status === "healthy" || item.feedback_status === "operator_retry_cooldown" ? (
<span className="muted">нет</span>
) : (
<button
type="button"
className="ghost"
onClick={() =>
void runAction(
() =>
client.expireFabricServiceChannelRouteFeedback(selectedClusterId, {
routeId: item.route_id,
reporterNodeId: item.reporter_node_id,
serviceClass: item.service_class,
reason: "operator expired stale service-channel feedback",
}),
"Service-channel feedback expired.",
)
}
>
expire
</button>
),
])}
/>
{fabricRouteFeedbackVisible.length === 0 && <EmptyState title="Feedback отсутствует" text="Нет свежих route feedback наблюдений от fabric service-channel runtime." />}
<DataTable
columns={["local node", "route", "replacement", "rebuild", "attempt", "feedback", "source", "destination", "decision", "score", "expires"]}
rows={[...fabricReplacementDecisions, ...fabricNoAlternateDecisions, ...fabricRebuildDecisions]
.filter((decision, index, all) => all.findIndex((item) => item.decision_id === decision.decision_id) === index)
.slice(0, 80)
.map((decision) => [
nodeName(nodes, decision.local_node_id),
shortId(decision.route_id),
decision.replacement_route_id ? shortId(decision.replacement_route_id) : "нет",
decision.rebuild_status || "нет",
decision.rebuild_attempt == null ? "н/д" : String(decision.rebuild_attempt),
decision.feedback_observation_id ? (
<div className="stackedText">
<span>{statusLabel(decision.feedback_source || "feedback")}</span>
<span className="muted">{shortId(decision.feedback_observation_id)} {decision.feedback_channel_id ? `ch ${shortId(decision.feedback_channel_id)}` : ""}</span>
<span className="muted">{statusLabel(decision.feedback_violation_status || "")}</span>
</div>
) : "нет",
nodeName(nodes, decision.source_node_id),
nodeName(nodes, decision.destination_node_id),
decision.decision_source,
decision.path_score == null ? "н/д" : String(decision.path_score),
formatDate(decision.expires_at),
])}
/>
<div className="inlineForm">
<label>
reporter
<select
value={fabricRebuildLedgerFilters.reporterNodeId}
onChange={(event) => setFabricRebuildLedgerFilters((filters) => ({ ...filters, reporterNodeId: event.target.value, offset: 0 }))}
>
<option value="">all</option>
{nodes.map((node) => (
<option key={node.id} value={node.id}>{node.name}</option>
))}
</select>
</label>
<label>
route
<input
value={fabricRebuildLedgerFilters.routeId}
onChange={(event) => setFabricRebuildLedgerFilters((filters) => ({ ...filters, routeId: event.target.value.trim(), offset: 0 }))}
placeholder="route id"
/>
</label>
<label>
generation
<input
value={fabricRebuildLedgerFilters.generation}
onChange={(event) => setFabricRebuildLedgerFilters((filters) => ({ ...filters, generation: event.target.value.trim(), offset: 0 }))}
placeholder="route generation"
/>
</label>
<label>
service
<input
value={fabricRebuildLedgerFilters.serviceClass}
onChange={(event) => setFabricRebuildLedgerFilters((filters) => ({ ...filters, serviceClass: event.target.value.trim(), offset: 0 }))}
placeholder="vpn_packets"
/>
</label>
<label>
feedback source
<input
value={fabricRebuildLedgerFilters.feedbackSource}
onChange={(event) => setFabricRebuildLedgerFilters((filters) => ({ ...filters, feedbackSource: event.target.value.trim(), offset: 0 }))}
placeholder="fabric_service_channel_access_report"
/>
</label>
<label>
channel
<input
value={fabricRebuildLedgerFilters.feedbackChannelId}
onChange={(event) => setFabricRebuildLedgerFilters((filters) => ({ ...filters, feedbackChannelId: event.target.value.trim(), offset: 0 }))}
placeholder="feedback channel id"
/>
</label>
<label>
violation
<input
value={fabricRebuildLedgerFilters.feedbackViolationStatus}
onChange={(event) => setFabricRebuildLedgerFilters((filters) => ({ ...filters, feedbackViolationStatus: event.target.value.trim(), offset: 0 }))}
placeholder="fabric_route_send_failed_backend_fallback_blocked"
/>
</label>
<button type="button" className="ghost" onClick={() => void loadFabricRebuildLedger(fabricRebuildLedgerDeep, { ...fabricRebuildLedgerFilters, offset: 0 })}>
apply
</button>
<button
type="button"
className="ghost"
onClick={() => {
const cleared = { ...defaultFabricRebuildLedgerFilters };
setFabricRebuildLedgerFilters(cleared);
void loadFabricRebuildLedger(false, cleared);
}}
>
clear
</button>
</div>
<DataTable
columns={["time", "reporter", "route", "replacement", "feedback", "guard", "outcome", "backend", "agent", "route-gen", "traffic", "policy", "hops"]}
rows={fabricRebuildAttempts.slice(0, 80).map((attempt) => [
formatDate(attempt.updated_at),
nodeName(nodes, attempt.reporter_node_id),
shortId(attempt.route_id),
attempt.replacement_route_id ? shortId(attempt.replacement_route_id) : "нет",
attempt.feedback_observation_id ? (
<div className="stackedText">
<span>{statusLabel(attempt.feedback_source || "feedback")}</span>
<span className="muted">{shortId(attempt.feedback_observation_id)} {attempt.feedback_channel_id ? `ch ${shortId(attempt.feedback_channel_id)}` : ""}</span>
<span className="muted">{statusLabel(attempt.feedback_violation_status || "")}</span>
</div>
) : attempt.feedback_status ? statusLabel(attempt.feedback_status) : "нет",
fabricRebuildLedgerDeep ? (
<span className={`pill ${attempt.guard_severity === "bad" ? "bad" : attempt.guard_severity === "warn" ? "warn" : "good"}`}>
{statusLabel(attempt.guard_status || "unknown")}{attempt.alert_silenced ? " / silenced" : attempt.alert_resurfaced ? " / resurfaced" : ""}
</span>
) : (
<span className="pill info">summary</span>
),
statusLabel(attempt.outcome),
<span className={`pill ${attempt.rebuild_status === "applied" ? "good" : "warn"}`}>{statusLabel(attempt.rebuild_status)}</span>,
!fabricRebuildLedgerDeep ? (
<span className="pill info">deep only</span>
) : attempt.node_transition_matched ? (
<span className={`pill ${attempt.node_transition_status === "applied_rebuild" ? "good" : "warn"}`}>{statusLabel(attempt.node_transition_status || "matched")}</span>
) : (
<span className="pill warn">not seen</span>
),
!fabricRebuildLedgerDeep ? (
<span className="pill info">deep only</span>
) : attempt.node_route_generation_matched ? (
<span className="pill good">{statusLabel(attempt.node_route_generation_status || "seen")}</span>
) : (
<span className="pill warn">not seen</span>
),
!fabricRebuildLedgerDeep
? "deep only"
: attempt.post_rebuild_selected_route_id
? `${shortId(attempt.post_rebuild_selected_route_id)} packets ${attempt.post_rebuild_send_flow_packets || attempt.post_rebuild_send_packets || 0} drop ${attempt.post_rebuild_send_flow_dropped || 0}`
: "нет",
attempt.policy_fingerprint ? shortId(attempt.policy_fingerprint) : "нет",
`${(attempt.old_hops || []).map((id) => nodeName(nodes, id)).join(" -> ") || "нет"} => ${(attempt.replacement_hops || []).map((id) => nodeName(nodes, id)).join(" -> ") || "нет"}`,
])}
/>
<div className="inlineActions">
<button
type="button"
className="ghost"
onClick={() => void loadFabricRebuildLedger(!fabricRebuildLedgerDeep, { ...fabricRebuildLedgerFilters, offset: 0 })}
>
{fabricRebuildLedgerDeep ? "fast ledger" : "deep ledger"}
</button>
<button
type="button"
className="ghost"
disabled={!fabricRebuildLedgerDeep || fabricRebuildLedgerFilters.offset <= 0}
onClick={() =>
void loadFabricRebuildLedger(true, {
...fabricRebuildLedgerFilters,
offset: Math.max(0, fabricRebuildLedgerFilters.offset - 20),
})
}
>
prev
</button>
<button
type="button"
className="ghost"
disabled={!fabricRebuildLedgerDeep || fabricRebuildAttempts.length < 20}
onClick={() => void loadFabricRebuildLedger(true, { ...fabricRebuildLedgerFilters, offset: fabricRebuildLedgerFilters.offset + 20 })}
>
next
</button>
<span className="pill">offset {fabricRebuildLedgerDeep ? fabricRebuildLedgerFilters.offset : 0}</span>
<span className="muted">Deep ledger correlates heartbeat timeline and can be slower; default refresh stays fast.</span>
</div>
{fabricRebuildAttempts.length === 0 && <EmptyState title="Rebuild ledger пуст" text="Пока нет долговечной истории service-channel route rebuild решений." />}
</article>
<article className="card span2">
<h3>{t.servicePlacement}</h3>
<DataTable
columns={["узел", "runtime", "адрес", "здоровье", "роли", "желаемые / reported сервисы", "последний heartbeat"]}
rows={nodes.map((node) => {
const runtime = nodeRuntimeConnectivity(node, heartbeatsByNode[node.id] || [], meshLinks);
return [
node.name,
<RuntimeBadges runtime={runtime} />,
runtime.address,
node.health_status,
summarizeRoles(rolesByNode[node.id] || []),
summarizeWorkloads(workloadsByNode[node.id] || []),
formatDate((heartbeatsByNode[node.id] || [])[0]?.observed_at || node.last_seen_at),
];
})}
/>
</article>
<article className="card">
<h3>{t.trafficFlow}</h3>
<DataTable
columns={["источник", "цель", "тип", "route/path", "статус", "задержка", "качество", "наблюдение"]}
rows={latestLinkObservations(meshLinks).filter((link) => link.source_node_id !== link.target_node_id).map((link) => {
const sourceNode = nodes.find((node) => node.id === link.source_node_id);
const targetNode = nodes.find((node) => node.id === link.target_node_id);
return [
<NodeLinkEndpoint node={sourceNode} fallback={nodeName(nodes, link.source_node_id)} heartbeatsByNode={heartbeatsByNode} meshLinks={meshLinks} />,
<NodeLinkEndpoint node={targetNode} fallback={nodeName(nodes, link.target_node_id)} heartbeatsByNode={heartbeatsByNode} meshLinks={meshLinks} />,
meshLinkObservationLabel(link),
meshLinkRoutePathSummary(link, nodes),
link.link_status,
link.latency_ms == null ? "н/д" : `${link.latency_ms} мс`,
link.quality_score == null ? "н/д" : `${link.quality_score}/100`,
formatDate(link.observed_at),
];
})}
/>
</article>
<article className="card">
<h3>Политики QoS</h3>
<DataTable
columns={["класс", "приоритет", "надежность", "политика сброса"]}
rows={qosPolicies.map((policy) => [
policy.service_class,
String(policy.priority),
policy.reliability_mode,
policy.drop_policy,
])}
/>
</article>
</section>
)}
{activeView === "vpn" && (
<section className="grid two">
<article className="card">
<h3>Создать желаемое состояние VPN-подключения</h3>
<p className="muted">Только control-plane. Здесь не выполняются TUN/TAP, маршруты, DNS, firewall, QoS или packet forwarding.</p>
<FormGrid>
<label>
ID организации
<input value={vpnForm.organizationId} onChange={(event) => setVPNForm({ ...vpnForm, organizationId: event.target.value })} />
</label>
<label>
Название
<input value={vpnForm.name} onChange={(event) => setVPNForm({ ...vpnForm, name: event.target.value })} />
</label>
<label>
Протокол
<select value={vpnForm.protocolFamily} onChange={(event) => setVPNForm({ ...vpnForm, protocolFamily: event.target.value })}>
<option value="generic">generic</option>
<option value="wireguard">wireguard</option>
<option value="ipsec">ipsec</option>
<option value="openvpn">openvpn</option>
</select>
</label>
<label>
Желаемое состояние
<select value={vpnForm.desiredState} onChange={(event) => setVPNForm({ ...vpnForm, desiredState: event.target.value })}>
<option value="disabled">выключено</option>
<option value="enabled">включено</option>
</select>
</label>
<label>
Ссылка на credential
<input value={vpnForm.credentialRef} onChange={(event) => setVPNForm({ ...vpnForm, credentialRef: event.target.value })} />
</label>
</FormGrid>
<label>
Целевой endpoint JSON
<textarea value={vpnForm.targetEndpointJson} onChange={(event) => setVPNForm({ ...vpnForm, targetEndpointJson: event.target.value })} />
</label>
<label>
Политика разрешенных узлов JSON
<textarea
value={vpnForm.allowedNodePolicyJson}
onChange={(event) => setVPNForm({ ...vpnForm, allowedNodePolicyJson: event.target.value })}
/>
</label>
<details>
<summary>Расширенные routing / QoS / placement JSON</summary>
<label>
Использование маршрутизации JSON
<textarea value={vpnForm.routingUsageJson} onChange={(event) => setVPNForm({ ...vpnForm, routingUsageJson: event.target.value })} />
</label>
<label>
Политика маршрута JSON
<textarea value={vpnForm.routePolicyJson} onChange={(event) => setVPNForm({ ...vpnForm, routePolicyJson: event.target.value })} />
</label>
<label>
Политика QoS JSON
<textarea value={vpnForm.qosPolicyJson} onChange={(event) => setVPNForm({ ...vpnForm, qosPolicyJson: event.target.value })} />
</label>
<label>
Политика размещения JSON
<textarea value={vpnForm.placementPolicyJson} onChange={(event) => setVPNForm({ ...vpnForm, placementPolicyJson: event.target.value })} />
</label>
</details>
<button
className="primary"
disabled={!selectedClusterId || !vpnForm.organizationId || !vpnForm.name}
onClick={() =>
void runAction(
() =>
client.createVPNConnection(selectedClusterId, {
organizationId: vpnForm.organizationId,
name: vpnForm.name,
protocolFamily: vpnForm.protocolFamily,
credentialRef: vpnForm.credentialRef || null,
desiredState: vpnForm.desiredState,
targetEndpoint: parseJSONObject(vpnForm.targetEndpointJson, "target endpoint"),
allowedNodePolicy: parseJSONObject(vpnForm.allowedNodePolicyJson, "allowed node policy"),
routingUsage: parseJSONArray(vpnForm.routingUsageJson, "routing usage"),
routePolicy: parseJSONObject(vpnForm.routePolicyJson, "route policy"),
qosPolicy: parseJSONObject(vpnForm.qosPolicyJson, "qos policy"),
placementPolicy: parseJSONObject(vpnForm.placementPolicyJson, "placement policy"),
}),
"Желаемое состояние VPN создано.",
)
}
>
Создать желаемое состояние VPN
</button>
</article>
<article className="card">
<div className="cardHead">
<div>
<h3>VPN-подключения</h3>
<p className="muted">Cluster-managed состояние, gateway packet stats и диагностика Android-клиента.</p>
</div>
<div className="actions compactActions">
<button
onClick={() =>
void runAction(async () => {
const expired = await client.expireStaleVPNLeases(selectedClusterId);
setNotice(`Истекшие VPN lease: ${expired.length}.`);
}, "Stale VPN lease проверены.")
}
>
Проверить stale lease
</button>
<button onClick={() => void refreshVPNClientDiagnostic()}>Обновить клиент</button>
</div>
</div>
<div className="inlineForm">
<label>
Android device id
<input
value={vpnDiagnosticDeviceId}
placeholder="0315f630-..."
onChange={(event) => setVPNDiagnosticDeviceId(event.target.value)}
onBlur={() => localStorage.setItem(storageKeys.vpnDiagnosticDeviceId, vpnDiagnosticDeviceId.trim())}
/>
</label>
{vpnClientDiagnostics.length > 0 && (
<label>
Найденные клиенты
<select
value={vpnDiagnosticDeviceId}
onChange={(event) => {
const deviceId = event.target.value;
setVPNDiagnosticDeviceId(deviceId);
localStorage.setItem(storageKeys.vpnDiagnosticDeviceId, deviceId);
setVPNClientDiagnostic(vpnClientDiagnostics.find((item) => item.device_id === deviceId) || null);
}}
>
{vpnClientDiagnostics.map((item) => {
const payload = objectField(item.payload) || {};
return (
<option key={item.device_id} value={item.device_id}>
{shortId(item.device_id)} / {stringField(payload, "app_version", "н/д")} / {formatDate(item.observed_at)}
</option>
);
})}
</select>
</label>
)}
</div>
<div className="diagnosticCommandPanel">
<label>
URL для теста
<input value={vpnDiagnosticTestUrl} onChange={(event) => setVPNDiagnosticTestUrl(event.target.value)} />
</label>
<div className="actions compactActions">
<button onClick={() => void sendVPNDiagnosticCommand({ type: "refresh_profile" }, "Профиль")}>Обновить профиль</button>
<button onClick={() => void sendVPNDiagnosticCommand({ type: "start_vpn" }, "VPN")}>Старт VPN</button>
<button onClick={() => void sendVPNDiagnosticCommand({ type: "stop_vpn" }, "VPN")}>Стоп VPN</button>
<button onClick={() => void sendVPNDiagnosticCommand({ type: "vpn_stats" }, "Stats")}>Stats</button>
<button onClick={() => void sendVPNDiagnosticCommand({ type: "vpn_http_get", url: vpnDiagnosticTestUrl }, "VPN HTTP")}>VPN HTTP</button>
<button onClick={() => void sendVPNDiagnosticCommand({ type: "open_url", url: vpnDiagnosticTestUrl }, "Открыть URL")}>Открыть URL</button>
<button
className="primary"
onClick={() =>
void sendVPNDiagnosticCommand({ type: "full_vpn_test", url: vpnDiagnosticTestUrl, watch_seconds: 45 }, "Полный VPN test")
}
>
Полный тест
</button>
</div>
{lastVPNDiagnosticCommand && (
<p className="muted">
Последняя команда: {stringField(lastVPNDiagnosticCommand.payload, "type", "н/д")} / {formatDate(lastVPNDiagnosticCommand.created_at)}
</p>
)}
</div>
{renderVPNClientDiagnostic(vpnClientDiagnostic)}
<div className="stack">
{vpnConnections.map((connection) => {
const runtime = objectField(connection.metadata?.client_config);
const fabricRoute = objectField(runtime?.vpn_fabric_route);
const entryPool = arrayStrings(fabricRoute?.entry_pool_node_ids || connection.placement_policy?.entry_node_ids);
const exitPool = arrayStrings(fabricRoute?.exit_pool_node_ids || connection.placement_policy?.exit_node_ids);
const selectedEntryNodeId = String(fabricRoute?.selected_entry_node_id || entryPool[0] || "");
const selectedExitNodeId = String(
fabricRoute?.selected_exit_node_id ||
vpnLeases[connection.id]?.owner_node_id ||
connection.placement_policy?.exit_node_id ||
exitPool[0] ||
"",
);
const stats = vpnPacketStats[connection.id] || {};
return (
<div className="vpnCard" key={connection.id}>
<div>
<strong>{connection.name}</strong>
<p className="muted">
{connection.protocol_family} / {connection.mode} / организация {shortId(connection.organization_id)}
</p>
<Status value={connection.desired_state} />
<Status value={connection.status} />
<span className={`pill ${runtime?.packet_forwarding ? "good" : "warn"}`}>
{runtime?.packet_forwarding ? "gateway packet relay active" : "gateway packet relay inactive"}
</span>
<span className="pill">
{String(fabricRoute?.preferred_data_plane || "backend_relay")} / fallback {String(fabricRoute?.fallback_data_plane || "н/д")}
</span>
</div>
<div className="stateList">
<StateLine label="Секрет" value={connection.credential_ref ? "задан" : "не задан"} />
<StateLine label="Активный lease" value={vpnLeases[connection.id] ? shortId(vpnLeases[connection.id]?.owner_node_id) : "нет"} />
<StateLine
label="Fabric route"
value={`${selectedEntryNodeId ? nodeName(nodes, selectedEntryNodeId) : "entry auto"} -> ${selectedExitNodeId ? nodeName(nodes, selectedExitNodeId) : "exit auto"}`}
/>
<StateLine label="Entry pool" value={entryPool.map((id) => nodeName(nodes, id)).join(", ") || "н/д"} />
<StateLine label="Exit pool" value={exitPool.map((id) => nodeName(nodes, id)).join(", ") || "н/д"} />
<StateLine label="Runtime" value={String(runtime?.runtime_status || "н/д")} />
<StateLine label="Gateway" value={String(runtime?.gateway_assignment_status || "н/д")} />
<StateLine label="Client -> gateway" value={vpnPacketStatLine(stats.client_to_gateway)} />
<StateLine label="Gateway -> client" value={vpnPacketStatLine(stats.gateway_to_client)} />
<StateLine label="Обновлено" value={formatDate(connection.updated_at)} />
</div>
<div className="actions">
<button
disabled={connection.desired_state === "enabled"}
onClick={() =>
void runAction(
() => client.updateVPNConnectionDesiredState(selectedClusterId, connection.id, "enabled"),
"Желаемое состояние VPN включено.",
)
}
>
Включить
</button>
<button
disabled={connection.desired_state === "disabled"}
onClick={() =>
void runAction(
() => client.updateVPNConnectionDesiredState(selectedClusterId, connection.id, "disabled"),
"Желаемое состояние VPN выключено.",
)
}
>
Выключить
</button>
</div>
</div>
);
})}
{vpnConnections.length === 0 && <EmptyState title="Нет желаемого состояния VPN" text="Control-plane записи C18 появятся здесь." />}
</div>
</article>
</section>
)}
{activeView === "org-safe" && (
<section className="grid two">
<article className="card span2">
<div className="cardHead">
<div>
<h3>Организации и пользователи</h3>
<p className="muted">Операционный слой для владельца платформы: tenant scope, роли участников и безопасная сводка без раскрытия core mesh.</p>
</div>
<span className="pill">{organizations.length}</span>
</div>
<FormGrid>
<label>
Slug
<input value={organizationForm.slug} onChange={(event) => setOrganizationForm({ ...organizationForm, slug: event.target.value })} placeholder="home" />
</label>
<label>
Название
<input value={organizationForm.name} onChange={(event) => setOrganizationForm({ ...organizationForm, name: event.target.value })} placeholder="HOME" />
</label>
</FormGrid>
<div className="actions">
<button
className="primary"
disabled={!organizationForm.slug.trim() || !organizationForm.name.trim()}
onClick={() =>
void runAction(async () => {
const org = await client.createOrganization(organizationForm);
setOrganizationForm({ slug: "", name: "" });
setOrganizationId(org.id);
setMembershipForm((previous) => ({ ...previous, organizationId: org.id }));
setResourceForm((previous) => ({ ...previous, organizationId: org.id }));
}, "Организация создана.")
}
>
Создать организацию
</button>
</div>
<DataTable
columns={["организация", "slug", "статус", "ресурсы", "участники", "действие"]}
rows={organizations.map((org) => {
const orgResources = resources.filter((resource) => resource.organization_id === org.id);
const memberships = membershipsByOrg[org.id] || [];
return [
org.name,
org.slug,
<Status value={org.status} />,
String(orgResources.length),
String(memberships.length),
<div className="actions" key={org.id}>
<button
onClick={() =>
void runAction(async () => {
setOrganizationId(org.id);
setOrganizationSummary(await client.getOrganizationAdminSummary(org.id));
}, "Сводка организации загружена.")
}
>
Открыть
</button>
</div>,
];
})}
/>
</article>
<article className="card">
<h3>Пользователь</h3>
<FormGrid>
<label>
Email / логин
<input value={userForm.email} onChange={(event) => setUserForm({ ...userForm, email: event.target.value })} placeholder="user@example.com" />
</label>
<label>
Пароль
<input
type="password"
value={userForm.password}
onChange={(event) => setUserForm({ ...userForm, password: event.target.value })}
placeholder="минимум 8 символов"
/>
</label>
<label>
Роль платформы
<select value={userForm.platformRole} onChange={(event) => setUserForm({ ...userForm, platformRole: event.target.value })}>
<option value="user">user</option>
<option value="platform_admin">platform_admin</option>
<option value="platform_recovery_admin">platform_recovery_admin</option>
</select>
</label>
</FormGrid>
<div className="actions">
<button
disabled={!userForm.email.trim() || userForm.password.length < 8}
onClick={() =>
void runAction(async () => {
const user = await client.createUser(userForm);
setUsers(await client.listUsers());
setUserForm({ email: "", password: "", platformRole: "user" });
setMembershipForm((previous) => ({ ...previous, userId: user.id }));
}, "Пользователь создан.")
}
>
Создать пользователя
</button>
</div>
<DataTable
columns={["пользователь", "роль платформы", "id"]}
rows={users.map((user) => [user.email, <Status value={user.platform_role || "user"} />, <code>{user.id}</code>])}
/>
</article>
<article className="card">
<h3>Участник организации</h3>
<FormGrid>
<label>
Организация
<select value={membershipForm.organizationId} onChange={(event) => setMembershipForm({ ...membershipForm, organizationId: event.target.value })}>
<option value="">Выберите организацию</option>
{organizations.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
</label>
<label>
Пользователь
<select value={membershipForm.userId} onChange={(event) => setMembershipForm({ ...membershipForm, userId: event.target.value })}>
<option value="">Выберите пользователя</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.email}
</option>
))}
</select>
</label>
<label>
Роль
<select value={membershipForm.roleId} onChange={(event) => setMembershipForm({ ...membershipForm, roleId: event.target.value })}>
<option value="org_owner">org_owner</option>
<option value="org_admin">org_admin</option>
<option value="org_operator">org_operator</option>
<option value="org_member">org_member</option>
<option value="org_viewer">org_viewer</option>
</select>
</label>
</FormGrid>
<div className="actions">
<button
disabled={!membershipForm.organizationId || !membershipForm.userId.trim()}
onClick={() =>
void runAction(
() => client.addOrganizationMembership(membershipForm.organizationId, { userId: membershipForm.userId, roleId: membershipForm.roleId }),
"Участник организации сохранен.",
)
}
>
Сохранить участника
</button>
</div>
</article>
<article className="card">
<h3>Безопасная сводка</h3>
<div className="inlineForm">
<select value={organizationId} onChange={(event) => setOrganizationId(event.target.value)}>
<option value="">Выберите организацию</option>
{organizations.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
<button
disabled={!organizationId}
onClick={() =>
void runAction(async () => {
setOrganizationSummary(await client.getOrganizationAdminSummary(organizationId));
}, "Сводка организации загружена.")
}
>
Обновить
</button>
</div>
{organizationSummary ? (
<div className="stack">
<Metric label="Ресурсы" value={organizationSummary.resource_count} tone="steel" />
<Metric label="Активные сессии" value={organizationSummary.active_session_count} tone="green" />
<StateLine label="Topology exposure" value={organizationSummary.topology_exposure} />
<DataTable
columns={["контур", "состояние"]}
rows={Object.entries(organizationSummary.connector_status || {}).map(([key, value]) => [
key,
typeof value === "string" ? statusLabel(value) : JSON.stringify(value),
])}
/>
<DataTable columns={["протокол", "количество"]} rows={organizationSummary.service_endpoints.map((item) => [item.protocol, String(item.count)])} />
</div>
) : (
<EmptyState title="Сводка не выбрана" text="Выберите организацию, чтобы проверить tenant-safe состояние." />
)}
</article>
</section>
)}
{activeView === "servers" && (
<section className="grid two">
<article className="card span2">
<div className="cardHead">
<div>
<h3>Справочник серверов</h3>
<p className="muted">Единый каталог целей для RDP/VPN: адрес сервера, организация, протокол и предпочтительный вход/выход маршрута.</p>
</div>
<span className="pill">{resources.length}</span>
</div>
<FormGrid>
<label>
Организация
<select value={resourceForm.organizationId} onChange={(event) => setResourceForm({ ...resourceForm, organizationId: event.target.value })}>
<option value="">Выберите организацию</option>
{organizations.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
</label>
<label>
Имя сервера
<input value={resourceForm.name} onChange={(event) => setResourceForm({ ...resourceForm, name: event.target.value })} placeholder="Office RDP" />
</label>
<label>
Адрес
<input value={resourceForm.address} onChange={(event) => setResourceForm({ ...resourceForm, address: event.target.value })} placeholder="192.168.1.10:3389" />
</label>
<label>
Протокол
<select value={resourceForm.protocol} onChange={(event) => setResourceForm({ ...resourceForm, protocol: event.target.value })}>
<option value="rdp">RDP</option>
<option value="vpn">VPN</option>
<option value="ssh">SSH</option>
<option value="http">HTTP</option>
</select>
</label>
<label>
Вход
<select value={resourceForm.entryNode} onChange={(event) => setResourceForm({ ...resourceForm, entryNode: event.target.value })}>
<option value="">Автоматически</option>
{nodes.map((node) => (
<option key={node.id} value={node.id}>
{node.name}
</option>
))}
</select>
</label>
<label>
Выход
<select value={resourceForm.exitNode} onChange={(event) => setResourceForm({ ...resourceForm, exitNode: event.target.value })}>
<option value="">Автоматически</option>
{nodes.map((node) => (
<option key={node.id} value={node.id}>
{node.name}
</option>
))}
</select>
</label>
<label>
Теги
<input value={resourceForm.tags} onChange={(event) => setResourceForm({ ...resourceForm, tags: event.target.value })} placeholder="home, accounting" />
</label>
<label>
RDP пользователь
<input value={resourceForm.username} onChange={(event) => setResourceForm({ ...resourceForm, username: event.target.value })} placeholder="user или DOMAIN\\user" />
</label>
<label>
RDP пароль
<input type="password" value={resourceForm.password} onChange={(event) => setResourceForm({ ...resourceForm, password: event.target.value })} placeholder="хранится как secret" />
</label>
<label>
Домен
<input value={resourceForm.domain} onChange={(event) => setResourceForm({ ...resourceForm, domain: event.target.value })} placeholder="опционально" />
</label>
</FormGrid>
<div className="actions">
<button
className="primary"
disabled={!resourceForm.organizationId || !resourceForm.name.trim() || !resourceForm.address.trim()}
onClick={() =>
void runAction(async () => {
const secretRef =
["rdp", "vnc", "ssh"].includes(resourceForm.protocol)
? `rap-secret://org/${resourceForm.organizationId}/resources/${crypto.randomUUID()}/primary`
: null;
const created = await client.createResource({
organizationId: resourceForm.organizationId,
name: resourceForm.name,
address: resourceForm.address,
protocol: resourceForm.protocol,
secretRef,
certificateVerificationMode: resourceForm.protocol === "rdp" ? "ignore" : "strict",
clipboardMode: resourceForm.protocol === "rdp" ? "bidirectional" : "disabled",
fileTransferMode: resourceForm.protocol === "rdp" ? "bidirectional" : "disabled",
metadata: {
route_mode: resourceForm.routeMode,
preferred_entry_node_id: resourceForm.entryNode || null,
preferred_exit_node_id: resourceForm.exitNode || null,
tags: resourceForm.tags.split(",").map((item) => item.trim()).filter(Boolean),
},
});
if (["rdp", "vnc", "ssh"].includes(resourceForm.protocol) && (resourceForm.username.trim() || resourceForm.password)) {
await client.upsertResourceSecret(created.id, {
username: resourceForm.username.trim(),
password: resourceForm.password,
domain: resourceForm.domain.trim(),
});
}
setResourceForm({ ...resourceForm, name: "", address: "", tags: "", username: "", password: "", domain: "" });
}, "Сервер добавлен в справочник.")
}
>
Добавить сервер
</button>
</div>
<DataTable
columns={["сервер", "адрес", "протокол", "секрет", "организация", "маршрут", "создано", "действия"]}
rows={resources.map((resource) => {
const metadata = resource.metadata || {};
const org = organizations.find((item) => item.id === resource.organization_id);
return [
resource.name,
resource.address,
resource.protocol,
resource.has_secret ? "сохранен" : resource.secret_ref ? "нужен payload" : "нет",
org?.name || shortId(resource.organization_id),
`${shortId(String(metadata.preferred_entry_node_id || "")) || "auto"} -> ${shortId(String(metadata.preferred_exit_node_id || "")) || "auto"}`,
formatDate(resource.created_at),
<button
className="ghost"
onClick={() => {
setResourceSecretDialog(resource);
setResourceSecretForm({ username: "", password: "", domain: "" });
}}
>
Обновить secret
</button>,
];
})}
/>
{resourceSecretDialog && (
<div className="modalBackdrop" role="presentation">
<div className="modalCard" role="dialog" aria-modal="true" aria-labelledby="resource-secret-title">
<div className="cardHead">
<div>
<h3 id="resource-secret-title">Учетные данные RDP</h3>
<p className="muted">
{resourceSecretDialog.name} · {resourceSecretDialog.address}
</p>
</div>
<button className="ghost" onClick={() => setResourceSecretDialog(null)}>
Закрыть
</button>
</div>
<FormGrid>
<label>
Пользователь
<input value={resourceSecretForm.username} onChange={(event) => setResourceSecretForm({ ...resourceSecretForm, username: event.target.value })} placeholder="user или DOMAIN\\user" />
</label>
<label>
Пароль
<input type="password" value={resourceSecretForm.password} onChange={(event) => setResourceSecretForm({ ...resourceSecretForm, password: event.target.value })} />
</label>
<label>
Домен
<input value={resourceSecretForm.domain} onChange={(event) => setResourceSecretForm({ ...resourceSecretForm, domain: event.target.value })} placeholder="опционально" />
</label>
</FormGrid>
<p className="muted">Пароль сохраняется как encrypted resource secret. В metadata ресурса он не попадет.</p>
<div className="actions">
<button
className="primary"
disabled={!resourceSecretForm.username.trim() || !resourceSecretForm.password}
onClick={() =>
void runAction(async () => {
await client.upsertResourceSecret(resourceSecretDialog.id, {
username: resourceSecretForm.username.trim(),
password: resourceSecretForm.password,
domain: resourceSecretForm.domain.trim(),
});
setResourceSecretDialog(null);
setResourceSecretForm({ username: "", password: "", domain: "" });
}, "Secret ресурса обновлен.")
}
>
Сохранить secret
</button>
<button onClick={() => setResourceSecretDialog(null)}>Отмена</button>
</div>
</div>
</div>
)}
</article>
</section>
)}
{activeView === "audit" && (
<section className="card">
<h3>Аудит кластера</h3>
<DataTable
columns={["событие", "цель", "actor", "создано"]}
rows={audit.map((event) => [
event.event_type,
`${event.target_type}${event.target_id ? `:${shortId(event.target_id)}` : ""}`,
event.actor_user_id ? shortId(event.actor_user_id) : "system",
formatDate(event.created_at),
])}
/>
</section>
)}
</section>
</main>
);
}
function Metric({ label, value, tone }: { label: string; value: number; tone: string }) {
return (
<article className={`metric ${tone}`}>
<span>{label}</span>
<strong>{value}</strong>
</article>
);
}
function Signal({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="signal">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
function Status({ value }: { value: string }) {
return <span className={`status ${value.replace(/_/g, "-")}`}>{statusLabel(value)}</span>;
}
function FunctionState({ label, value, tone }: { label: string; value: string; tone?: string }) {
return (
<span className={`functionState ${tone || ""}`}>
<small>{label}</small>
<strong>{value}</strong>
</span>
);
}
function StateLine({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="stateLine">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
function EmptyState({ title, text }: { title: string; text: string }) {
return (
<article className="empty">
<h3>{title}</h3>
<p>{text}</p>
</article>
);
}
function renderVPNClientDiagnostic(status: VPNClientDiagnosticStatus | null) {
if (!status) {
return <p className="muted">Диагностика Android-клиента не загружена. Укажи device id из приложения и нажми Обновить клиент.</p>;
}
const payload = objectField(status.payload) || {};
const runtime = objectField(payload.runtime);
const config = objectField(payload.vpn_config);
const appVersion = stringField(payload, "app_version", "н/д");
const serviceState = stringField(payload, "service_state", "н/д");
const controlMode = stringField(payload, "control_network_mode", "н/д");
const activeRelay = stringField(config, "packet_relay_active_base_url") || stringField(config, "packet_relay_base_url", "н/д");
const profileRelay = stringField(config, "packet_relay_profile_base_url", "н/д");
const candidates = stringField(config, "packet_relay_candidate_urls", "н/д");
const read = numberField(runtime, "uplink_read_total");
const sent = numberField(runtime, "uplink_sent_total");
const down = numberField(runtime, "downlink_received_total");
const drops = numberField(runtime, "uplink_dropped_packets") + numberField(runtime, "downlink_dropped_packets");
const bypass = numberField(runtime, "uplink_bypassed_control_packets");
const downBytes = numberField(runtime, "downlink_received_bytes");
const upBytes = numberField(runtime, "uplink_sent_bytes");
const runtimeState = stringField(runtime, "state", "н/д");
const runtimeMessage = stringField(runtime, "message", "");
const upMbps = numberField(runtime, "uplink_sent_mbps");
const downMbps = numberField(runtime, "downlink_received_mbps");
const lastCommandType = stringField(payload, "last_command_type", "н/д");
const lastCommandResult = stringField(payload, "last_command_result", "н/д");
return (
<div className="vpnCard diagnosticCard">
<div>
<strong>Android client {shortId(status.device_id)}</strong>
<p className="muted">
{appVersion} / {serviceState} / {formatDate(status.observed_at)}
</p>
<Status value={Date.now() - new Date(status.observed_at).getTime() < 30_000 ? "active" : "degraded"} />
<span className="pill">{controlMode}</span>
</div>
<div className="stateList">
<StateLine label="Relay active" value={activeRelay} />
<StateLine label="Relay profile" value={profileRelay} />
<StateLine label="Relay candidates" value={candidates} />
<StateLine label="Packets read/sent/down" value={`${read} / ${sent} / ${down}`} />
<StateLine label="Drops / control bypass" value={`${drops} / ${bypass}`} />
<StateLine label="Bytes up/down" value={`${formatBytes(upBytes)} / ${formatBytes(downBytes)}`} />
<StateLine label="Rate up/down" value={`${upMbps.toFixed(2)} / ${downMbps.toFixed(2)} Mbps`} />
<StateLine label="Runtime" value={runtimeMessage ? `${runtimeState}: ${runtimeMessage}` : runtimeState} />
<StateLine label="Last command" value={`${lastCommandType}: ${lastCommandResult}`} />
</div>
</div>
);
}
function TelemetryStrip({ items, emptyText }: { items: NodeTelemetryObservation[]; emptyText: string }) {
if (items.length === 0) {
return <EmptyState title={emptyText} text="Тестовая телеметрия появится здесь после отчета node-agent." />;
}
const ordered = [...items].reverse().slice(-24);
const latest = items[0];
const maxMemory = Math.max(...ordered.map((item) => item.memory_used_bytes || 0), 1);
return (
<div className="telemetryBox">
<div className="signalStrip compact">
<Signal label="Память" value={`${formatBytes(latest.memory_used_bytes)} / ${formatBytes(latest.memory_total_bytes)}`} />
<Signal label="Процессор" value={latest.cpu_percent == null ? "н/д" : `${latest.cpu_percent.toFixed(1)}%`} />
<Signal label="Процессы" value={latest.process_count == null ? "н/д" : String(latest.process_count)} />
<Signal label="Обновлено" value={formatDate(latest.observed_at)} />
</div>
<div className="sparkline" aria-label="memory telemetry">
{ordered.map((item) => (
<span key={item.id} style={{ height: `${Math.max(8, Math.round(((item.memory_used_bytes || 0) / maxMemory) * 100))}%` }} />
))}
</div>
</div>
);
}
function NodeDetailsDashboard({
node,
memberships,
activeRoles,
desiredWorkloads,
observedWorkloads,
heartbeats,
telemetry,
meshLinks,
syntheticConfig,
allNodes,
onSetUpdatePolicy,
updatePlan,
updateStatuses,
labels,
}: {
node: ClusterNode;
memberships: Array<{ cluster: Cluster; node: ClusterNode }>;
activeRoles: RoleAssignment[];
desiredWorkloads: NodeWorkloadDesiredState[];
observedWorkloads: WorkloadStatus[];
heartbeats: NodeHeartbeat[];
telemetry: NodeTelemetryObservation[];
meshLinks: MeshLink[];
syntheticConfig?: NodeSyntheticMeshConfig;
allNodes: ClusterNode[];
onSetUpdatePolicy?: (node: ClusterNode, product: string, targetVersion: string | null) => void;
updatePlan?: NodeUpdatePlan;
updateStatuses: NodeUpdateStatus[];
labels: Record<string, string>;
}) {
const latestHeartbeat = heartbeats[0];
const latestTelemetry = telemetry[0];
const listenerReport = objectField(latestHeartbeat?.metadata?.mesh_listener_report);
const endpointReport = objectField(latestHeartbeat?.metadata?.mesh_endpoint_report);
const outboundReport = objectField(latestHeartbeat?.metadata?.mesh_outbound_session_report);
const desiredListenerConfig = syntheticConfig?.mesh_listener;
const recoveryReport = objectField(latestHeartbeat?.metadata?.mesh_peer_recovery_report);
const intentReport = objectField(latestHeartbeat?.metadata?.mesh_peer_connection_intent_report);
const managerReport = objectField(latestHeartbeat?.metadata?.mesh_peer_connection_manager_report);
const rendezvousReport = objectField(latestHeartbeat?.metadata?.mesh_rendezvous_lease_report);
const routeDecisionReport = objectField(latestHeartbeat?.metadata?.mesh_route_path_decision_report);
const routeGenerationReport = objectField(latestHeartbeat?.metadata?.mesh_route_generation_report);
const routeHealthReport = objectField(latestHeartbeat?.metadata?.mesh_route_health_config_report);
const serviceChannelFeedback = syntheticConfig?.service_channel_route_feedback;
const serviceChannelFeedbackItems = serviceChannelFeedback?.observations || [];
const serviceChannelRemediationCommands = syntheticConfig?.service_channel_remediation_commands || [];
const latestLinks = latestLinkObservations(meshLinks).filter((link) => link.source_node_id !== link.target_node_id);
const reachableLinks = latestLinks.filter((link) => link.link_status === "reachable");
const failedLinks = latestLinks.filter((link) => link.link_status !== "reachable");
const capabilityEntries = Object.entries(latestHeartbeat?.capabilities || {}).sort(([left], [right]) => left.localeCompare(right));
const managerProbeResults = arrayObjects(managerReport?.probe_results);
const [activeTab, setActiveTab] = useState("network");
const latestNodeAgentUpdate = latestUpdateStatus(updateStatuses, "rap-node-agent");
const latestHostAgentUpdate = latestUpdateStatus(updateStatuses, "rap-host-agent");
const latestAnyUpdate = updateStatuses[0];
const updaterHealth = nodeUpdaterSummary(updateStatuses);
const primaryClusterID = memberships.find((membership) => membership.node.id === node.id)?.cluster.id || memberships[0]?.cluster.id || "";
const reportedCandidates = arrayObjects(endpointReport?.endpoint_candidates);
const primaryReportedCandidate = reportedCandidates[0];
const advertisedEndpoint =
firstStringField(endpointReport, ["peer_endpoint", "advertised_endpoint", "endpoint"]) ||
stringField(primaryReportedCandidate, "address", "") ||
"";
const reportedTransport =
firstStringField(endpointReport, ["transport", "advertise_transport"]) ||
stringField(primaryReportedCandidate, "transport", "") ||
"н/д";
const reportedConnectivity =
firstStringField(endpointReport, ["connectivity_mode", "connectivity"]) ||
stringField(primaryReportedCandidate, "connectivity_mode", "") ||
stringField(listenerReport, "inbound_reachability", "") ||
"н/д";
const reportedNat = stringField(endpointReport, "nat_type", stringField(primaryReportedCandidate, "nat_type", "н/д"));
const reportedRegion = stringField(endpointReport, "region", stringField(listenerReport, "region", stringField(primaryReportedCandidate, "region", "н/д")));
const reportedObservedAt = stringField(endpointReport, "observed_at", stringField(listenerReport, "observed_at", latestHeartbeat?.observed_at || "н/д"));
const listenerStatus =
stringField(listenerReport, "status", "") ||
(advertisedEndpoint ? "нет listener report, есть advertised endpoint" : "report отсутствует");
const listenerEffectiveAddr =
stringField(listenerReport, "effective_listen_addr", "") ||
"н/д";
const listenerConfiguredAddr =
stringField(listenerReport, "configured_listen_addr", "") ||
"н/д";
const listenerCandidates =
reportedCandidates.length > 0
? reportedCandidates
: advertisedEndpoint
? [
{
endpoint_id: `${node.id}-reported`,
address: advertisedEndpoint,
transport: reportedTransport,
reachability: reportedConnectivity,
connectivity_mode: reportedConnectivity,
nat_type: reportedNat,
priority: "н/д",
last_verified_at: reportedObservedAt,
},
]
: [];
const configuredPeerEndpoints = Object.entries(syntheticConfig?.peer_endpoints || {});
const configuredPeerCandidates = Object.entries(syntheticConfig?.peer_endpoint_candidates || {}).flatMap(([peerID, candidates]) =>
candidates.map((candidate) => ({ peerID, candidate })),
);
const activePeerIDs = new Set(reachableLinks.map((link) => (link.source_node_id === node.id ? link.target_node_id : link.source_node_id)));
const reservePeerCandidates = configuredPeerCandidates.filter(({ peerID }) => !activePeerIDs.has(peerID));
const networkDiagnostics = [
listenerReport ? "listener report: есть" : "listener report: не прислан агентом",
endpointReport ? "endpoint report: есть" : "endpoint report: не прислан агентом",
outboundReport ? "outbound session: есть" : "outbound session: не прислан агентом",
syntheticConfig ? `scoped config: ${syntheticConfig.enabled ? "enabled" : "disabled"}` : "scoped config: не загружен",
serviceChannelFeedback ? `service-channel feedback: ${serviceChannelFeedback.observation_count}` : "service-channel feedback: не загружен",
`active links: ${reachableLinks.length}/${latestLinks.length}`,
];
const tabs = [
["overview", "Обзор"],
["network", "Сеть и адреса"],
["mesh", "Mesh"],
["services", "Роли и сервисы"],
["telemetry", "Телеметрия"],
["updates", "Обновления"],
["raw", "Raw"],
] as const;
return (
<div className="nodeDetails">
<section className="nodePanel">
<h4>Сводка runtime</h4>
<div className="signalStrip compact nodeMetricGrid">
<Signal label="Heartbeat" value={latestHeartbeat ? formatDate(latestHeartbeat.observed_at) : "н/д"} />
<Signal label="Health" value={statusLabel(latestHeartbeat?.health_status || node.health_status)} />
<Signal label="Listener" value={meshListenerSummary(latestHeartbeat)} />
<Signal label="Mesh links" value={`${reachableLinks.length}/${latestLinks.length}`} />
<Signal label="Update" value={updateStatusSummary(latestAnyUpdate, updatePlan)} />
</div>
<div className="summaryChips">
<Status value={node.registration_status} />
<Status value={node.membership_status} />
<Status value={node.partition_state} />
<span className="pill">{node.reported_version || latestHeartbeat?.reported_version || "версия неизвестна"}</span>
{listenerReport?.one_way_connectivity === true && <span className="pill warn">one-way</span>}
{listenerReport?.port_conflict === true && <span className="pill bad">port conflict</span>}
</div>
</section>
<div className="nodeTabs" role="tablist" aria-label="Node analysis sheets">
{tabs.map(([id, label]) => (
<button key={id} className={activeTab === id ? "active" : ""} onClick={() => setActiveTab(id)} type="button">
{label}
</button>
))}
</div>
{activeTab === "overview" && (
<div className="nodeDetailGrid">
<section className="nodePanel">
<h4>Идентичность и размещение</h4>
<div className="stateList">
<StateLine label="Node ID" value={node.id} />
<StateLine label="Node key" value={node.node_key} />
<StateLine label="Имя" value={node.name} />
<StateLine label="Владение" value={statusLabel(node.ownership_type)} />
<StateLine label="Owner org" value={shortId(node.owner_organization_id)} />
<StateLine label="Группа" value={node.node_group_name || labels.ungroupedNodes} />
<StateLine label="Создан" value={formatDate(node.created_at)} />
<StateLine label="Обновлен" value={formatDate(node.updated_at)} />
</div>
</section>
<section className="nodePanel">
<h4>Участие в кластерах</h4>
<div className="membershipList">
{memberships.map((membership) => (
<span key={membership.cluster.id} className={membership.node.id === node.id && membership.node.membership_status === "active" ? "pill good" : "pill"}>
{membership.cluster.name}: {statusLabel(membership.node.membership_status)}
</span>
))}
</div>
<div className="stateList">
<StateLine label="Активных ролей" value={String(activeRoles.length)} />
<StateLine label="Desired workloads" value={String(desiredWorkloads.length)} />
<StateLine label="Observed workloads" value={String(observedWorkloads.length)} />
<StateLine label="Последний сигнал" value={formatDate(node.last_seen_at || latestHeartbeat?.observed_at)} />
</div>
</section>
</div>
)}
{activeTab === "network" && (
<>
<div className="nodeDetailGrid">
<section className="nodePanel">
<h4>Локальный listener</h4>
<div className="stateList">
<StateLine label="Статус" value={listenerStatus} />
<StateLine label="Режим порта" value={stringField(listenerReport, "listen_port_mode", "н/д")} />
<StateLine label="Configured addr" value={listenerConfiguredAddr} />
<StateLine label="Effective addr" value={listenerEffectiveAddr} />
<StateLine label="Inbound" value={stringField(listenerReport, "inbound_reachability", reportedConnectivity)} />
<StateLine label="One-way" value={stringField(listenerReport, "one_way_connectivity", "н/д")} />
<StateLine label="Port conflict" value={stringField(listenerReport, "port_conflict", "false")} />
<StateLine label="Failure" value={stringField(listenerReport, "failure_error", stringField(listenerReport, "failure_reason", "нет"))} />
</div>
</section>
<section className="nodePanel">
<h4>Desired listener</h4>
<div className="stateList">
<StateLine label="Состояние" value={desiredListenerConfig?.desired_state || "н/д"} />
<StateLine label="Режим порта" value={desiredListenerConfig?.listen_port_mode || "н/д"} />
<StateLine label="Listen addr" value={desiredListenerConfig?.listen_addr || "н/д"} />
<StateLine label="Auto range" value={desiredListenerConfig ? `${desiredListenerConfig.auto_port_start || "н/д"}-${desiredListenerConfig.auto_port_end || "н/д"}` : "н/д"} />
<StateLine label="Advertise endpoint" value={desiredListenerConfig?.advertise_endpoint || "auto-discovery"} />
<StateLine label="Advertise transport" value={desiredListenerConfig?.advertise_transport || "н/д"} />
<StateLine label="Connectivity" value={desiredListenerConfig?.connectivity_mode || "н/д"} />
<StateLine label="NAT" value={desiredListenerConfig?.nat_type || "н/д"} />
<StateLine label="Region/site" value={desiredListenerConfig?.region || "н/д"} />
<StateLine label="Version" value={desiredListenerConfig?.config_version || "н/д"} />
</div>
</section>
<section className="nodePanel">
<h4>Что узел сообщает кластеру</h4>
<div className="stateList">
<StateLine label="Advertised endpoint" value={advertisedEndpoint || "не прислан"} />
<StateLine label="Transport" value={reportedTransport} />
<StateLine label="Connectivity" value={reportedConnectivity} />
<StateLine label="NAT" value={reportedNat} />
<StateLine label="Region/site" value={reportedRegion} />
<StateLine label="Observed" value={reportedObservedAt} />
</div>
</section>
</div>
<section className="nodePanel">
<h4>Исходящий control-channel</h4>
<div className="stateList">
<StateLine label="Status" value={stringField(outboundReport, "status", "не прислан")} />
<StateLine label="Direction" value={stringField(outboundReport, "direction", "н/д")} />
<StateLine label="Transport" value={stringField(outboundReport, "transport", "н/д")} />
<StateLine label="Control Plane" value={stringField(outboundReport, "control_plane_url", "н/д")} />
<StateLine label="Reverse usable" value={stringField(outboundReport, "usable_for_inbound_control", "н/д")} />
<StateLine label="Inbound required" value={stringField(outboundReport, "inbound_listener_required", "н/д")} />
<StateLine label="Relay ready" value={stringField(outboundReport, "peer_connection_relay_ready", "0")} />
<StateLine label="Waiting rendezvous" value={stringField(outboundReport, "peer_connection_waiting", "0")} />
<StateLine label="Rendezvous leases" value={stringField(outboundReport, "rendezvous_lease_count", "0")} />
<StateLine label="Listener conflict" value={stringField(outboundReport, "listener_port_conflict", "false")} />
</div>
</section>
<section className="nodePanel">
<h4>Наличие сетевых отчетов</h4>
<div className="summaryChips">
{networkDiagnostics.map((item) => (
<span key={item} className={item.includes("не прислан") || item.includes("не загружен") ? "pill warn" : "pill good"}>
{item}
</span>
))}
</div>
{!endpointReport && !listenerReport && (
<p className="muted">
У этого узла есть heartbeat/mesh manager данные, но агент не передал адресный отчет. До обновления агента или включения endpoint/listener report панель может показать связи и config peers, но не может достоверно назвать локальный listen address.
</p>
)}
</section>
<section className="nodePanel">
<h4>Endpoint candidates узла</h4>
<DataTable
columns={["id", "address", "transport", "reachability", "mode", "nat", "priority", "verified"]}
rows={listenerCandidates.map((candidate) => [
stringField(candidate, "endpoint_id", "н/д"),
stringField(candidate, "address", "н/д"),
stringField(candidate, "transport", "н/д"),
stringField(candidate, "reachability", "н/д"),
stringField(candidate, "connectivity_mode", "н/д"),
stringField(candidate, "nat_type", "н/д"),
stringField(candidate, "priority", "н/д"),
stringField(candidate, "last_verified_at", "н/д"),
])}
/>
</section>
<div className="nodeDetailGrid">
<section className="nodePanel">
<h4>Рабочие peer endpoints из config</h4>
<DataTable
columns={["peer", "endpoint"]}
rows={configuredPeerEndpoints.map(([peerID, endpoint]) => [nodeName(allNodes, peerID), endpoint])}
/>
</section>
<section className="nodePanel">
<h4>Резервные кандидаты peer</h4>
<DataTable
columns={["peer", "address", "transport", "reachability", "mode", "priority"]}
rows={reservePeerCandidates.slice(0, 20).map(({ peerID, candidate }) => [
nodeName(allNodes, peerID),
candidate.address,
candidate.transport,
candidate.reachability,
candidate.connectivity_mode,
String(candidate.priority),
])}
/>
</section>
</div>
<section className="nodePanel">
<h4>Активные связи этого узла</h4>
<DataTable
columns={["peer", "направление", "тип", "статус", "latency", "quality", "путь", "наблюдение"]}
rows={latestLinks.slice(0, 20).map((link) => {
const peerID = link.source_node_id === node.id ? link.target_node_id : link.source_node_id;
return [
nodeName(allNodes, peerID),
link.source_node_id === node.id ? "out" : "in",
meshLinkObservationLabel(link),
link.link_status,
link.latency_ms == null ? "н/д" : `${link.latency_ms}мс`,
link.quality_score == null ? "н/д" : String(link.quality_score),
meshLinkRoutePathSummary(link, allNodes),
formatDate(link.observed_at),
];
})}
/>
{failedLinks.length > 0 && <p className="muted">Проблемных связей: {failedLinks.length}. Их статус виден в таблице выше.</p>}
</section>
<section className="nodePanel">
<h4>Проверка адресов peer-to-peer</h4>
<DataTable
columns={["peer", "status", "selected endpoint", "candidate", "latency", "attempts", "failure"]}
rows={managerProbeResults.slice(0, 20).map((result) => [
nodeName(allNodes, stringField(result, "node_id", "")),
stringField(result, "link_status", "н/д"),
stringField(result, "selected_endpoint", stringField(result, "endpoint", "н/д")),
stringField(result, "selected_candidate_id", "н/д"),
stringField(result, "latency_ms", "н/д"),
peerCandidateProbeSummary(result),
stringField(result, "failure_reason", "нет"),
])}
/>
</section>
</>
)}
{activeTab === "mesh" && (
<>
<div className="nodeDetailGrid">
<section className="nodePanel">
<h4>Mesh control-plane</h4>
<div className="stateList">
<StateLine label="Recovery" value={meshRecoverySummary(latestHeartbeat)} />
<StateLine label="Intents" value={meshConnectionIntentSummary(latestHeartbeat)} />
<StateLine label="Manager" value={meshConnectionManagerSummary(latestHeartbeat)} />
<StateLine label="Rendezvous" value={meshRendezvousLeaseSummary(latestHeartbeat)} />
<StateLine label="Path decisions" value={meshRoutePathDecisionSummary(latestHeartbeat)} />
<StateLine label="Route generation" value={meshRouteGenerationSummary(latestHeartbeat)} />
<StateLine label="Route health" value={meshRouteHealthConfigSummary(latestHeartbeat)} />
<StateLine
label="Service-channel feedback"
value={
serviceChannelFeedback
? `${serviceChannelFeedback.healthy_route_count} healthy / ${serviceChannelFeedback.degraded_route_count} degraded / ${serviceChannelFeedback.fenced_route_count} fenced`
: "н/д"
}
/>
<StateLine
label="Recovery policy"
value={
serviceChannelFeedback?.recovery_policy
? `${serviceChannelFeedback.recovery_policy.source} p${serviceChannelFeedback.recovery_policy.hysteresis_penalty} promote ${serviceChannelFeedback.recovery_policy.promotion_min_samples}`
: "н/д"
}
/>
<StateLine
label="Route policy"
value={
syntheticConfig?.route_path_decisions?.recovery_policy
? `${syntheticConfig.route_path_decisions.recovery_policy.source} fail/drop/slow ${syntheticConfig.route_path_decisions.recovery_policy.demotion_failure_threshold}/${syntheticConfig.route_path_decisions.recovery_policy.demotion_drop_threshold}/${syntheticConfig.route_path_decisions.recovery_policy.demotion_slow_threshold}`
: "н/д"
}
/>
<StateLine label="Config version" value={syntheticConfig?.config_version || "н/д"} />
</div>
</section>
<section className="nodePanel">
<h4>Scoped config counts</h4>
<div className="stateList">
<StateLine label="Peer endpoints" value={String(configuredPeerEndpoints.length)} />
<StateLine label="Endpoint candidates" value={String(configuredPeerCandidates.length)} />
<StateLine label="Peer directory" value={String(syntheticConfig?.peer_directory?.length || 0)} />
<StateLine label="Recovery seeds" value={String(syntheticConfig?.recovery_seeds?.length || 0)} />
<StateLine label="Rendezvous leases" value={String(syntheticConfig?.rendezvous_leases?.length || 0)} />
<StateLine label="Routes" value={String(syntheticConfig?.routes?.length || 0)} />
<StateLine label="Fenced routes" value={String(serviceChannelFeedback?.fenced_route_count || 0)} />
<StateLine label="Remediation commands" value={String(serviceChannelRemediationCommands.length)} />
<StateLine
label="Feedback provenance"
value={
serviceChannelFeedback
? `missing ${serviceChannelFeedback.missing_provenance_count || 0} / stale policy ${serviceChannelFeedback.stale_policy_count || 0} / stale gen ${serviceChannelFeedback.stale_generation_count || 0}`
: "н/д"
}
/>
</div>
</section>
</div>
<section className="nodePanel">
<h4>Route decisions</h4>
<DataTable
columns={["route", "replacement", "source", "destination", "effective hops", "decision", "score", "expires"]}
rows={(syntheticConfig?.route_path_decisions?.decisions || []).map((decision) => [
shortId(decision.route_id),
decision.replacement_route_id ? shortId(decision.replacement_route_id) : "нет",
nodeName(allNodes, decision.source_node_id),
nodeName(allNodes, decision.destination_node_id),
decision.effective_hops.map((id) => trimNodeName(nodeName(allNodes, id))).join(" > "),
decision.decision_source || (decision.selected_relay_id ? nodeName(allNodes, decision.selected_relay_id) : "direct"),
decision.path_score == null ? "н/д" : String(decision.path_score),
formatDate(decision.expires_at),
])}
/>
</section>
{serviceChannelRemediationCommands.length > 0 && (
<section className="nodePanel">
<h4>Service-channel remediation commands</h4>
<DataTable
columns={["channel", "action", "primary", "replacement", "guard", "execution", "reason", "expires"]}
rows={serviceChannelRemediationCommands.slice(0, 20).map((command) => [
shortId(command?.channel_id || ""),
<span className="pill warn">{statusLabel(command?.action || "")}</span>,
command?.primary_route_id ? shortId(command.primary_route_id) : "н/д",
command?.replacement_route_id ? shortId(command.replacement_route_id) : "н/д",
<span className={`pill ${command?.guard_status === "rejected" ? "bad" : command?.guard_status === "allowed" ? "good" : ""}`}>
{command?.guard_status ? statusLabel(command.guard_status) : "н/д"}
</span>,
<span className={`pill ${remediationExecutionTone(command?.execution_status)}`}>
{command?.execution_status ? statusLabel(command.execution_status) : "н/д"}
{command?.execution_reason ? ` / ${statusLabel(command.execution_reason)}` : ""}
</span>,
command?.reason || "н/д",
command?.expires_at ? formatDate(command.expires_at) : "н/д",
])}
/>
</section>
)}
<section className="nodePanel">
<h4>Service-channel route feedback</h4>
<DataTable
columns={["route", "service", "status", "recovery", "score", "reasons", "failures", "duration", "expires"]}
rows={serviceChannelFeedbackItems.slice(0, 40).map((item) => [
shortId(item.route_id),
item.service_class,
<span className={`pill ${serviceChannelFeedbackTone(item.feedback_status)}`}>{statusLabel(item.feedback_status)}</span>,
item.recovery_state ? (
<span className={`pill ${serviceChannelRecoveryTone(item.recovery_state)}`}>
{item.recovery_demoted ? `demoted ${item.recovery_reason ? statusLabel(item.recovery_reason) : ""}` : item.recovery_promoted ? "promoted" : statusLabel(item.recovery_state)}
{item.recovery_hysteresis_penalty ? ` -${item.recovery_hysteresis_penalty}` : ""}
</span>
) : item.stale_policy || item.stale_generation ? (
<span className="pill warn">{statusLabel(item.stale_reason || "stale")}</span>
) : item.provenance_missing ? (
<span className="pill warn">provenance missing</span>
) : (
"нет"
),
String(item.score_adjustment),
(item.reasons || []).join(", ") || "нет",
String(item.consecutive_failures || 0),
item.last_send_duration_ms == null ? "н/д" : `${item.last_send_duration_ms}мс`,
formatDate(item.expires_at),
])}
/>
{serviceChannelFeedbackItems.length === 0 && (
<p className="muted">Пока нет свежих наблюдений. Узел будет присылать их после реального traffic через service-channel runtime.</p>
)}
</section>
</>
)}
{activeTab === "services" && (
<>
<div className="nodeDetailGrid">
<section className="nodePanel">
<h4>{labels.nodeRoles}</h4>
<div className="serviceTags">
{activeRoles.length === 0 && <p className="muted">{labels.noRoles}</p>}
{activeRoles.map((role) => (
<div className="serviceTag" key={role.id}>
<strong>{roleDisplayLabel(role.role)}</strong>
<span>{role.organization_id ? `organization: ${shortId(role.organization_id)}` : "cluster-wide"}</span>
<small>{formatDate(role.assigned_at)}</small>
<span className={`pill ${capabilityPillTone(role.role, latestHeartbeat)}`}>{capabilityLabel(role.role, latestHeartbeat, labels)}</span>
</div>
))}
</div>
</section>
<section className="nodePanel">
<h4>Capabilities</h4>
<div className="summaryChips">
{capabilityEntries.length === 0 && <span className="muted">Нет capability heartbeat.</span>}
{capabilityEntries.slice(0, 40).map(([key, value]) => (
<span className={value === true ? "pill good" : "pill"} key={key}>
{key}
</span>
))}
</div>
</section>
</div>
<div className="nodeDetailGrid">
<section className="nodePanel">
<h4>{labels.desiredServices}</h4>
<DataTable
columns={["service", "desired", "runtime", "version", "updated"]}
rows={desiredWorkloads.map((workload) => [
workload.service_type,
statusLabel(workload.desired_state),
workload.runtime_mode,
workload.version || "не закреплена",
formatDate(workload.updated_at),
])}
/>
</section>
<section className="nodePanel">
<h4>{labels.observedServices}</h4>
<DataTable
columns={["service", "reported", "runtime", "version", "observed"]}
rows={observedWorkloads.map((workload) => [
workload.service_type,
statusLabel(workload.reported_state),
workload.runtime_mode,
workload.version || "н/д",
formatDate(workload.observed_at),
])}
/>
</section>
</div>
</>
)}
{activeTab === "telemetry" && (
<>
<section className="nodePanel">
<h4>{labels.nodeTelemetry}</h4>
<TelemetryStrip items={telemetry} emptyText={labels.noTelemetry} />
<div className="stateList">
<StateLine label="Disk" value={`${formatBytes(latestTelemetry?.disk_used_bytes)} / ${formatBytes(latestTelemetry?.disk_total_bytes)}`} />
<StateLine label="Network RX/TX" value={`${formatBytes(latestTelemetry?.network_rx_bytes)} / ${formatBytes(latestTelemetry?.network_tx_bytes)}`} />
<StateLine label="Payload" value={latestTelemetry?.payload ? compactJSON(latestTelemetry.payload) : "н/д"} />
</div>
</section>
<section className="nodePanel">
<h4>{labels.recentHeartbeats}</h4>
<DataTable
columns={["состояние", "версия", "listener", "mesh recovery", "mesh intents", "rv leases", "path decisions", "route gen", "route health", "наблюдение"]}
rows={heartbeats.slice(0, 10).map((heartbeat) => [
heartbeat.health_status,
heartbeat.reported_version || "неизвестно",
meshListenerSummary(heartbeat),
meshRecoverySummary(heartbeat),
meshConnectionIntentSummary(heartbeat),
meshRendezvousLeaseSummary(heartbeat),
meshRoutePathDecisionSummary(heartbeat),
meshRouteGenerationSummary(heartbeat),
meshRouteHealthConfigSummary(heartbeat),
formatDate(heartbeat.observed_at),
])}
/>
</section>
</>
)}
{activeTab === "updates" && (
<>
<div className="nodeDetailGrid">
<section className="nodePanel">
<h4>Текущая сборка</h4>
<div className="stateList">
<StateLine label="Node-agent version" value={node.reported_version || latestHeartbeat?.reported_version || "неизвестно"} />
<StateLine label="План" value={updatePlan ? `${updatePlan.action}: ${updatePlan.reason}` : "не загружен"} />
<StateLine label="Product" value={updatePlan?.product || "rap-node-agent"} />
<StateLine label="Target" value={updatePlan?.target_version || "н/д"} />
<StateLine label="Strategy" value={updatePlan?.strategy || "н/д"} />
<StateLine label="Rollback" value={updatePlan?.rollback_allowed ? "разрешен" : "нет"} />
<StateLine label="Artifact" value={updatePlan?.artifact ? `${updatePlan.artifact.kind} ${updatePlan.artifact.os}/${updatePlan.artifact.arch}` : "н/д"} />
</div>
<div className="actions">
<button className="primary" disabled={!onSetUpdatePolicy} onClick={() => onSetUpdatePolicy?.(node, "rap-node-agent", null)}>
Node-agent latest
</button>
<button className="ghost" disabled={!onSetUpdatePolicy || !updatePlan?.target_version} onClick={() => onSetUpdatePolicy?.(node, "rap-node-agent", updatePlan?.target_version || null)}>
Повторить target
</button>
<button className="ghost" disabled={!onSetUpdatePolicy} onClick={() => onSetUpdatePolicy?.(node, "rap-host-agent", null)}>
Host-agent latest
</button>
</div>
<p className="muted">
Latest означает policy без закрепленной версии: updater будет брать свежий active release своего канала при следующем цикле или heartbeat hint.
</p>
</section>
<section className="nodePanel">
<h4>Последние отчеты updater</h4>
<div className="stateList">
<StateLine label="Updater health" value={`${updaterHealth.label}: ${updaterHealth.detail}`} />
<StateLine label="rap-node-agent" value={updateStatusLine(latestNodeAgentUpdate)} />
<StateLine label="rap-host-agent" value={updateStatusLine(latestHostAgentUpdate)} />
<StateLine label="Всего отчетов" value={String(updateStatuses.length)} />
<StateLine label="Последний отчет" value={formatDate(latestAnyUpdate?.observed_at)} />
</div>
<div className="summaryChips">
<span className={`pill ${updaterHealth.tone}`}>{updaterHealth.label}</span>
{latestNodeAgentUpdate && <span className={`pill ${updateStatusTone(latestNodeAgentUpdate)}`}>node-agent: {latestNodeAgentUpdate.status}</span>}
{latestHostAgentUpdate && <span className={`pill ${updateStatusTone(latestHostAgentUpdate)}`}>host-agent: {latestHostAgentUpdate.status}</span>}
{!latestNodeAgentUpdate && !latestHostAgentUpdate && <span className="pill warn">updater пока не отчитался</span>}
</div>
</section>
</div>
<section className="nodePanel">
<h4>История обновлений</h4>
<DataTable
columns={["product", "current", "target", "phase", "status", "attempt", "error", "observed"]}
rows={updateStatuses.slice(0, 40).map((status) => [
status.product,
status.current_version || "н/д",
status.target_version || "н/д",
status.phase,
<span className={`pill ${updateStatusTone(status)}`}>{status.status}</span>,
status.attempt_id ? shortId(status.attempt_id) : "н/д",
status.error_message || "нет",
formatDate(status.observed_at),
])}
/>
</section>
<section className="nodePanel">
<h4>Windows repair/update command</h4>
<p className="muted">
Для существующего Windows-узла эта команда переустанавливает wrapper updater без нового join-token, сохраняет local state и запускает обновление до актуальной сборки.
</p>
<div className="stateList compact">
<StateLine label="Когда выполнять" value="если updater stale, host-agent не отчитался или Windows-узел не доходит до target version" />
<StateLine label="Control Plane" value={externalPreferredControlPlaneEndpoint()} />
<StateLine label="Join-token" value="не нужен для repair существующего узла" />
</div>
<div className="actions">
<button
className="primary"
onClick={() => downloadTextFile(windowsRepairUpdaterCmdFileName(node), windowsRepairUpdaterCmdCommand(node, primaryClusterID))}
>
Скачать repair .cmd
</button>
<button
className="ghost"
onClick={() => void copyTextToClipboard(windowsRepairUpdaterCmdCommand(node, primaryClusterID))}
>
Скопировать команду
</button>
</div>
<pre className="codePreview">{windowsRepairUpdaterCmdCommand(node, primaryClusterID)}</pre>
</section>
<section className="nodePanel">
<h4>Linux repair/update command</h4>
<p className="muted">
Для существующего Ubuntu/Linux-узла эта команда восстанавливает systemd updater без нового join-token, сохраняет local state и делает одноразовую проверку обновления.
</p>
<div className="stateList compact">
<StateLine label="Когда выполнять" value="если host-agent не отчитался, updater stale или Linux-узел не доходит до target version" />
<StateLine label="Control Plane" value={externalPreferredControlPlaneEndpoint()} />
<StateLine label="Join-token" value="не нужен для repair существующего узла" />
</div>
<div className="actions">
<button
className="primary"
onClick={() => downloadTextFile(linuxRepairUpdaterShFileName(node), linuxRepairUpdaterShCommand(node, primaryClusterID))}
>
Скачать repair .sh
</button>
<button
className="ghost"
onClick={() => void copyTextToClipboard(linuxRepairUpdaterShCommand(node, primaryClusterID))}
>
Скопировать команду
</button>
</div>
<pre className="codePreview">{linuxRepairUpdaterShCommand(node, primaryClusterID)}</pre>
</section>
<section className="nodePanel">
<h4>Payload последнего отчета</h4>
<div className="rawDetailsGrid">
<RawJSON title="rap-node-agent update status" value={latestNodeAgentUpdate} />
<RawJSON title="rap-host-agent update status" value={latestHostAgentUpdate} />
<RawJSON title="Update plan" value={updatePlan} />
</div>
</section>
</>
)}
{activeTab === "raw" && (
<section className="nodePanel">
<h4>Raw данные узла</h4>
<div className="rawDetailsGrid">
<RawJSON title="Последний heartbeat metadata" value={latestHeartbeat?.metadata} />
<RawJSON title="Heartbeat capabilities" value={latestHeartbeat?.capabilities} />
<RawJSON title="Heartbeat service states" value={latestHeartbeat?.service_states} />
<RawJSON title="Synthetic mesh config" value={syntheticConfig} />
<RawJSON title="Listener report" value={listenerReport} />
<RawJSON title="Endpoint report" value={endpointReport} />
<RawJSON title="Peer recovery report" value={recoveryReport} />
<RawJSON title="Connection intent report" value={intentReport} />
<RawJSON title="Connection manager report" value={managerReport} />
<RawJSON title="Rendezvous lease report" value={rendezvousReport} />
<RawJSON title="Route decision report" value={routeDecisionReport} />
<RawJSON title="Route generation report" value={routeGenerationReport} />
<RawJSON title="Route health report" value={routeHealthReport} />
</div>
</section>
)}
</div>
);
}
function RawJSON({ title, value }: { title: string; value: unknown }) {
return (
<details className="rawBlock">
<summary>{title}</summary>
<pre>{value == null ? "н/д" : JSON.stringify(value, null, 2)}</pre>
</details>
);
}
type NodeRuntimeConnectivity = {
agentLabel: string;
agentTone: string;
clientLabel: string;
clientTone: string;
outboundLabel: string;
outboundTone: string;
inboundLabel: string;
inboundTone: string;
address: string;
detail: string;
};
function RuntimeBadges({ runtime }: { runtime: NodeRuntimeConnectivity }) {
return (
<div className="runtimeBadges">
<span className={`pill ${runtime.agentTone}`}>{runtime.agentLabel}</span>
<span className={`pill ${runtime.clientTone}`}>{runtime.clientLabel}</span>
<span className={`pill ${runtime.outboundTone}`}>{runtime.outboundLabel}</span>
<span className={`pill ${runtime.inboundTone}`}>{runtime.inboundLabel}</span>
</div>
);
}
function NodeLinkEndpoint({
node,
fallback,
heartbeatsByNode,
meshLinks,
}: {
node?: ClusterNode;
fallback: string;
heartbeatsByNode: Record<string, NodeHeartbeat[]>;
meshLinks: MeshLink[];
}) {
if (!node) {
return fallback;
}
const runtime = nodeRuntimeConnectivity(node, heartbeatsByNode[node.id] || [], meshLinks);
return (
<div className="nodeEndpointCell">
<strong>{node.name}</strong>
<RuntimeBadges runtime={runtime} />
<small>{runtime.address}</small>
</div>
);
}
function FabricTopology({
nodes,
links,
syntheticMeshConfigsByNode,
entryPoints,
entryPointNodesById,
egressPools,
egressPoolNodesById,
rolesByNode,
workloadsByNode,
telemetryByNode,
labels,
emptyText,
}: {
nodes: ClusterNode[];
links: MeshLink[];
syntheticMeshConfigsByNode: Record<string, NodeSyntheticMeshConfig>;
entryPoints: FabricEntryPoint[];
entryPointNodesById: Record<string, FabricEntryPointNode[]>;
egressPools: FabricEgressPool[];
egressPoolNodesById: Record<string, FabricEgressPoolNode[]>;
rolesByNode: Record<string, RoleAssignment[]>;
workloadsByNode: Record<string, WorkloadStatus[]>;
telemetryByNode: Record<string, NodeTelemetryObservation[]>;
labels: Record<string, string>;
emptyText: string;
}) {
if (nodes.length === 0) {
return <EmptyState title="Нет узлов" text="Одобренные node-agent появятся на карте после первого heartbeat." />;
}
const latestLinks = latestLinkObservations(links).filter((link) => link.source_node_id !== link.target_node_id);
const nodesById = new Map(nodes.map((node) => [node.id, node] as const));
const topologyLinks = latestLinks.map((link) => ({ link, status: topologyLinkRuntimeStatus(link, latestLinks, nodesById) }));
const realLinks = topologyLinks.filter((item) => item.status === "reachable");
const oneWayLinks = topologyLinks.filter((item) => item.status === "one_way");
const staleLinks = topologyLinks.filter((item) => item.status === "stale");
const problemLinks = topologyLinks.filter((item) => item.status !== "reachable" && item.status !== "one_way" && item.status !== "stale");
const configuredLinks = configuredTopologyLinks(nodes, syntheticMeshConfigsByNode);
const viewBox = topologyViewBox(nodes.length, entryPoints.length, egressPools.length);
const nodeRadius = topologyNodeRadius(nodes.length);
const nodePositions = topologyNodePositions(nodes, viewBox.height, nodeRadius);
const entryPositions = new Map(entryPoints.map((entryPoint, index) => [entryPoint.id, topologyPoint(170, index, entryPoints.length, 150, viewBox.height - 100)] as const));
const egressPositions = new Map(egressPools.map((pool, index) => [pool.id, topologyPoint(950, index, egressPools.length, 150, viewBox.height - 100)] as const));
const observedPairs = new Set(topologyLinks.filter((item) => item.status !== "stale").map((item) => `${item.link.source_node_id}->${item.link.target_node_id}`));
const meshStatusByNode = new Map(nodes.map((node) => [node.id, topologyNodeMeshStatus(node.id, topologyLinks)] as const));
const visibleConfiguredLinks = configuredLinks.filter((link) => !observedPairs.has(`${link.source_node_id}->${link.target_node_id}`));
const activeEntryAssignments = entryPoints.flatMap((entryPoint) =>
(entryPointNodesById[entryPoint.id] || [])
.filter((assignment) => assignment.status !== "disabled")
.map((assignment) => ({ entryPoint, assignment })),
);
const activeEgressAssignments = egressPools.flatMap((pool) =>
(egressPoolNodesById[pool.id] || [])
.filter((assignment) => assignment.status !== "disabled")
.map((assignment) => ({ pool, assignment })),
);
const placementCount = activeEntryAssignments.length + activeEgressAssignments.length;
return (
<div className="topologyShell">
<svg className="topologySvg" viewBox={`0 0 ${viewBox.width} ${viewBox.height}`} role="img" aria-label="Карта трафика узлов Fabric">
<defs>
<marker id="arrow" markerHeight="8" markerWidth="8" orient="auto" refX="7" refY="4">
<path d="M0,0 L8,4 L0,8 Z" fill="currentColor" />
</marker>
</defs>
<rect x="36" y="58" width="268" height={viewBox.height - 100} rx="24" className="topologyZone ingress" />
<rect x="330" y="58" width="460" height={viewBox.height - 100} rx="24" className="topologyZone core" />
<rect x="816" y="58" width="268" height={viewBox.height - 100} rx="24" className="topologyZone egress" />
<text x="170" y="98" className="topologyLayerLabel">
{labels.fabricIngressLayer}
</text>
<text x="560" y="98" className="topologyLayerLabel">
{labels.fabricNodeLayer}
</text>
<text x="950" y="98" className="topologyLayerLabel">
{labels.fabricEgressLayer}
</text>
{activeEntryAssignments.map(({ entryPoint, assignment }) => {
const source = entryPositions.get(entryPoint.id);
const target = nodePositions.get(assignment.node_id);
if (!source || !target) {
return null;
}
return (
<line
key={`entry-${entryPoint.id}-${assignment.node_id}`}
x1={source.x + 78}
y1={source.y}
x2={target.x - nodeRadius - 8}
y2={target.y}
className={`topologyPlacementLink ${assignment.status === "active" ? "good" : "weak"}`}
markerEnd="url(#arrow)"
/>
);
})}
{activeEgressAssignments.map(({ pool, assignment }) => {
const source = nodePositions.get(assignment.node_id);
const target = egressPositions.get(pool.id);
if (!source || !target) {
return null;
}
return (
<line
key={`egress-${pool.id}-${assignment.node_id}`}
x1={source.x + nodeRadius + 8}
y1={source.y}
x2={target.x - 78}
y2={target.y}
className={`topologyPlacementLink ${assignment.status === "active" ? "good" : "weak"}`}
markerEnd="url(#arrow)"
/>
);
})}
{visibleConfiguredLinks.map((link) => {
const source = nodePositions.get(link.source_node_id);
const target = nodePositions.get(link.target_node_id);
if (!source || !target) {
return null;
}
const edge = topologyEdgePoints(source, target, nodeRadius + 8);
return (
<line
key={link.id}
x1={edge.x1}
y1={edge.y1}
x2={edge.x2}
y2={edge.y2}
className="topologyConfiguredLink"
markerEnd="url(#arrow)"
/>
);
})}
{topologyLinks.map(({ link, status }) => {
const source = nodePositions.get(link.source_node_id);
const target = nodePositions.get(link.target_node_id);
if (!source || !target) {
return null;
}
const edge = topologyEdgePoints(source, target, nodeRadius + 8);
const midX = (edge.x1 + edge.x2) / 2;
const midY = (edge.y1 + edge.y2) / 2;
return (
<g key={link.id || `${link.source_node_id}-${link.target_node_id}`}>
<line
x1={edge.x1}
y1={edge.y1}
x2={edge.x2}
y2={edge.y2}
className={`topologyLink ${linkTone(link, status)}`}
markerEnd="url(#arrow)"
/>
<text x={midX} y={midY - 8} className="topologyLinkLabel">
{meshLinkTopologyLabel(link, status)}
</text>
</g>
);
})}
{entryPoints.map((entryPoint) => {
const point = entryPositions.get(entryPoint.id)!;
const count = (entryPointNodesById[entryPoint.id] || []).length;
return (
<g key={entryPoint.id} className="topologyEndpoint">
<rect x={point.x - 86} y={point.y - 36} width="172" height="72" rx="20" className={`topologyEndpointRect ${entryPoint.status}`} />
<text x={point.x} y={point.y - 8} className="topologyEndpointName">
{trimNodeName(entryPoint.name)}
</text>
<text x={point.x} y={point.y + 15} className="topologyEndpointMeta">
{entryPoint.endpoint_type} · {count}
</text>
</g>
);
})}
{nodes.map((node) => {
const point = nodePositions.get(node.id)!;
const labelSize = topologyNodeLabelSize(nodes.length);
const activeRoles = activeRoleAssignments(rolesByNode[node.id] || []);
const meshStatus = meshStatusByNode.get(node.id) || "isolated";
return (
<g key={node.id} className="topologyNode">
<circle cx={point.x} cy={point.y} r={nodeRadius} className={`topologyNodeCircle ${node.health_status}`} />
<text x={point.x} y={point.y - labelSize.nameOffset} className="topologyNodeName" style={{ fontSize: labelSize.name }}>
{trimNodeName(node.name, labelSize.maxChars)}
</text>
<text x={point.x} y={point.y + labelSize.metaOffset} className="topologyNodeMeta" style={{ fontSize: labelSize.meta }}>
{activeRoles.length} активн. ролей / {(workloadsByNode[node.id] || []).length} серв.
</text>
<text x={point.x} y={point.y + labelSize.memoryOffset} className="topologyNodeMeta" style={{ fontSize: labelSize.meta }}>
mesh: {statusLabel(meshStatus)}
</text>
</g>
);
})}
{egressPools.map((pool) => {
const point = egressPositions.get(pool.id)!;
const count = (egressPoolNodesById[pool.id] || []).length;
return (
<g key={pool.id} className="topologyEndpoint">
<rect x={point.x - 86} y={point.y - 36} width="172" height="72" rx="20" className={`topologyEndpointRect ${pool.status}`} />
<text x={point.x} y={point.y - 8} className="topologyEndpointName">
{trimNodeName(pool.name)}
</text>
<text x={point.x} y={point.y + 15} className="topologyEndpointMeta">
egress · {count}
</text>
</g>
);
})}
{topologyLinks.length === 0 && visibleConfiguredLinks.length === 0 && placementCount === 0 && (
<text x={viewBox.width / 2} y={viewBox.height - 34} className="topologyEmpty">
{emptyText}
</text>
)}
</svg>
<div className="topologyLegend">
<span>
<i className="legendLine placement" /> {labels.placementIntent}: {placementCount}
</span>
<span>
<i className="legendLine observed" /> реальные: {realLinks.length}
</span>
<span>
<i className="legendLine oneWay" /> one-way: {oneWayLinks.length}
</span>
<span>
<i className="legendLine stale" /> stale: {staleLinks.length}
</span>
<span>
<i className="legendLine problem" /> проблемы: {problemLinks.length}
</span>
<span>
<i className="legendLine configured" /> configured: {visibleConfiguredLinks.length}
</span>
</div>
<div className="serviceTags">
{entryPoints.map((entryPoint) => (
<div className="serviceTag" key={entryPoint.id}>
<strong>{entryPoint.name}</strong>
<span>{entryPoint.endpoint_type}</span>
<small>{(entryPointNodesById[entryPoint.id] || []).map((item) => nodeName(nodes, item.node_id)).join(", ") || labels.assignedNodesEmpty}</small>
</div>
))}
{nodes.map((node) => (
<div className="serviceTag" key={node.id}>
<strong>{node.name}</strong>
<span>{statusLabel(node.health_status)} / mesh {statusLabel(meshStatusByNode.get(node.id) || "isolated")}</span>
<small>{summarizeRoles(rolesByNode[node.id] || [])}</small>
<small>{summarizeWorkloads(workloadsByNode[node.id] || [])}</small>
</div>
))}
{egressPools.map((pool) => (
<div className="serviceTag" key={pool.id}>
<strong>{pool.name}</strong>
<span>{pool.status}</span>
<small>{(egressPoolNodesById[pool.id] || []).map((item) => nodeName(nodes, item.node_id)).join(", ") || labels.assignedNodesEmpty}</small>
</div>
))}
</div>
</div>
);
}
type TopologyLinkStatus = "reachable" | "one_way" | "stale" | "degraded" | "unreachable" | "unknown";
function topologyNodeMeshStatus(nodeId: string, links: { link: MeshLink; status: TopologyLinkStatus }[]) {
const peerLinks = links.filter((item) => item.link.source_node_id !== item.link.target_node_id && (item.link.source_node_id === nodeId || item.link.target_node_id === nodeId));
if (peerLinks.some((item) => item.status === "reachable" || item.status === "one_way")) {
return "connected";
}
if (peerLinks.some((item) => item.status !== "stale")) {
return "degraded";
}
return "isolated";
}
function topologyLinkRuntimeStatus(link: MeshLink, links: MeshLink[], nodesById: Map<string, ClusterNode>): TopologyLinkStatus {
if (isMeshLinkStale(link, nodesById)) {
return "stale";
}
if (link.link_status !== "reachable") {
return link.link_status === "degraded" || link.link_status === "unreachable" ? link.link_status : "unknown";
}
const reverse = links.find((candidate) => candidate.source_node_id === link.target_node_id && candidate.target_node_id === link.source_node_id && !isMeshLinkStale(candidate, nodesById));
if (!reverse || reverse.link_status !== "reachable") {
return "one_way";
}
return "reachable";
}
function isMeshLinkStale(link: MeshLink, nodesById?: Map<string, ClusterNode>) {
if (link.link_status === "stale" || link.metadata?.derived_link_stale === true) {
return true;
}
const observedAt = new Date(link.observed_at).getTime();
if (!Number.isFinite(observedAt) || Date.now() - observedAt > 2 * 60 * 1000) {
return true;
}
if (!nodesById) {
return false;
}
const source = nodesById.get(link.source_node_id);
const target = nodesById.get(link.target_node_id);
return source?.health_status !== "healthy" || target?.health_status !== "healthy";
}
function topologyPoint(x: number, index: number, count: number, top: number, bottom: number) {
if (count <= 1) {
return { x, y: Math.round((top + bottom) / 2) };
}
return { x, y: Math.round(top + ((bottom - top) * index) / (count - 1)) };
}
function topologyViewBox(nodeCount: number, entryCount: number, egressCount: number) {
const maxColumnCount = Math.max(nodeCount, entryCount, egressCount, 1);
const rows = Math.max(Math.ceil(nodeCount / 3), maxColumnCount);
return {
width: 1120,
height: Math.max(640, 220 + rows * 104),
};
}
function topologyNodeRadius(nodeCount: number) {
if (nodeCount > 48) {
return 26;
}
if (nodeCount > 24) {
return 32;
}
if (nodeCount > 12) {
return 40;
}
return 52;
}
function topologyNodeLabelSize(nodeCount: number) {
if (nodeCount > 48) {
return { name: 12, meta: 9, nameOffset: 7, metaOffset: 7, memoryOffset: 20, maxChars: 10 };
}
if (nodeCount > 24) {
return { name: 14, meta: 10, nameOffset: 8, metaOffset: 9, memoryOffset: 24, maxChars: 12 };
}
if (nodeCount > 12) {
return { name: 16, meta: 12, nameOffset: 10, metaOffset: 11, memoryOffset: 28, maxChars: 14 };
}
return { name: 21, meta: 15, nameOffset: 12, metaOffset: 10, memoryOffset: 31, maxChars: 18 };
}
function topologyNodePositions(nodes: ClusterNode[], height: number, radius: number) {
const columns = nodes.length > 24 ? 4 : nodes.length > 8 ? 3 : nodes.length > 3 ? 2 : 1;
const rows = Math.max(1, Math.ceil(nodes.length / columns));
const left = 410;
const right = 710;
const xStep = columns === 1 ? 0 : (right - left) / (columns - 1);
const top = 142;
const bottom = height - 96;
const yStep = rows === 1 ? 0 : (bottom - top) / (rows - 1);
return new Map(
nodes.map((node, index) => {
const column = index % columns;
const row = Math.floor(index / columns);
return [
node.id,
{
x: Math.round(columns === 1 ? 560 : left + xStep * column),
y: Math.round(rows === 1 ? (top + bottom) / 2 : top + yStep * row),
},
] as const;
}),
);
}
function topologyEdgePoints(source: { x: number; y: number }, target: { x: number; y: number }, offset: number) {
const dx = target.x - source.x;
const dy = target.y - source.y;
const distance = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
const ox = (dx / distance) * offset;
const oy = (dy / distance) * offset;
return {
x1: source.x + ox,
y1: source.y + oy,
x2: target.x - ox,
y2: target.y - oy,
};
}
function configuredTopologyLinks(nodes: ClusterNode[], configsByNode: Record<string, NodeSyntheticMeshConfig>) {
const knownNodes = new Set(nodes.map((node) => node.id));
const byPair = new Map<string, { id: string; source_node_id: string; target_node_id: string }>();
for (const [nodeID, config] of Object.entries(configsByNode)) {
for (const route of config.routes || []) {
const hops = route.hops || [];
for (let index = 0; index < hops.length - 1; index++) {
const source = hops[index];
const target = hops[index + 1];
if (!knownNodes.has(source) || !knownNodes.has(target) || source === target) {
continue;
}
byPair.set(`${source}->${target}`, {
id: `configured-${nodeID}-${route.route_id}-${index}`,
source_node_id: source,
target_node_id: target,
});
}
}
}
return [...byPair.values()];
}
function NodeGroupTree({ groups, nodes, labels }: { groups: ClusterNodeGroup[]; nodes: ClusterNode[]; labels: Record<string, string> }) {
const rootGroups = groups
.filter((group) => !group.parent_group_id)
.sort((left, right) => left.sort_order - right.sort_order || left.name.localeCompare(right.name));
const ungroupedNodes = nodes.filter((node) => !node.node_group_id);
if (groups.length === 0 && nodes.length === 0) {
return <EmptyState title="Нет узлов" text="Одобренные node-agent появятся здесь." />;
}
return (
<div className="treeList">
{rootGroups.map((group) => (
<NodeGroupBranch key={group.id} group={group} groups={groups} nodes={nodes} depth={0} />
))}
{ungroupedNodes.length > 0 && (
<section className="treeBranch">
<h4>{labels.ungroupedNodes}</h4>
<div className="treeNodeList">
{ungroupedNodes.map((node) => (
<span className="pill" key={node.id}>
{node.name}
</span>
))}
</div>
</section>
)}
</div>
);
}
function NodeGroupBranch({ group, groups, nodes, depth }: { group: ClusterNodeGroup; groups: ClusterNodeGroup[]; nodes: ClusterNode[]; depth: number }) {
const childGroups = groups
.filter((item) => item.parent_group_id === group.id)
.sort((left, right) => left.sort_order - right.sort_order || left.name.localeCompare(right.name));
const groupedNodes = nodes.filter((node) => node.node_group_id === group.id);
return (
<section className="treeBranch" style={{ marginLeft: `${depth * 18}px` }}>
<h4>
{group.name}
<span className="pill">{groupedNodes.length}</span>
</h4>
{group.description && <p className="muted">{group.description}</p>}
<div className="treeNodeList">
{groupedNodes.map((node) => (
<span className="pill good" key={node.id}>
{node.name}
</span>
))}
</div>
{childGroups.map((child) => (
<NodeGroupBranch key={child.id} group={child} groups={groups} nodes={nodes} depth={depth + 1} />
))}
</section>
);
}
function FormGrid({ children }: { children: ReactNode }) {
return <div className="formGrid">{children}</div>;
}
function DataTable({ columns, rows }: { columns: string[]; rows: ReactNode[][] }) {
if (rows.length === 0) {
return <EmptyState title="Нет данных" text="В текущей области пока нечего показать." />;
}
return (
<div className="tableWrap">
<table>
<thead>
<tr>
{columns.map((column) => (
<th key={column}>{column}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={index}>
{row.map((cell, cellIndex) => (
<td key={`${index}-${cellIndex}`}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
function parseJSONObject(raw: string, label: string): Record<string, unknown> {
const parsed = JSON.parse(raw || "{}") as unknown;
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
throw new Error(`${label}: требуется JSON object.`);
}
return parsed as Record<string, unknown>;
}
function parseJSONArray(raw: string, label: string): unknown[] {
const parsed = JSON.parse(raw || "[]") as unknown;
if (!Array.isArray(parsed)) {
throw new Error(`${label}: требуется JSON array.`);
}
return parsed;
}
function summarizeRoles(roles: RoleAssignment[]) {
const activeRoles = activeRoleAssignments(roles);
if (activeRoles.length === 0) {
return "активные роли не назначены";
}
return activeRoles.map((role) => `${roleDisplayLabel(role.role)}${role.organization_id ? ` @ ${shortId(role.organization_id)}` : ""}`).join(", ");
}
function activeRoleAssignments(roles: RoleAssignment[]) {
return roles.filter((role) => role.status === "active");
}
function roleDisplayLabel(role: string) {
const name = roleDisplayNames[role];
return name ? `${name} (${role})` : role;
}
function summarizeWorkloads(workloads: WorkloadStatus[]) {
if (workloads.length === 0) {
return "нет сервисов";
}
return workloads.map((workload) => `${workload.service_type}:${workload.reported_state}`).join(", ");
}
function nodeUpdateSummary(node: ClusterNode, plan: NodeUpdatePlan | undefined, releases: ReleaseVersion[]) {
const latest =
releases.find((release) => release.product === "rap-node-agent" && release.channel === "stable" && release.status === "active") ||
releases.find((release) => release.product === "rap-node-agent" && release.status === "active");
const current = node.reported_version || "";
const target = plan?.target_version || latest?.version || "";
if (node.version_state && node.version_state !== "unknown") {
return { status: node.version_state, targetLabel: target ? `target ${target}` : "policy target unknown" };
}
if (!current) {
return { status: "unknown", targetLabel: target ? `target ${target}` : "target unknown" };
}
if (plan?.action === "update") {
return { status: "outdated", targetLabel: `target ${plan.target_version || target}` };
}
if (target && current !== target) {
return { status: "outdated", targetLabel: `latest ${target}` };
}
if (target && current === target) {
return { status: "current", targetLabel: `latest ${target}` };
}
return { status: plan?.reason === "no_update_policy" ? "no_policy" : "unknown", targetLabel: plan?.reason || "release policy unknown" };
}
function latestUpdateStatus(statuses: NodeUpdateStatus[], product: string) {
return statuses.find((status) => status.product === product);
}
function updateStatusSummary(status: NodeUpdateStatus | undefined, plan: NodeUpdatePlan | undefined) {
if (status) {
return `${status.product}: ${status.phase}/${status.status}`;
}
if (plan) {
return `${plan.action}: ${plan.reason}`;
}
return "нет отчета";
}
function updateStatusLine(status: NodeUpdateStatus | undefined) {
if (!status) {
return "нет отчета";
}
const target = status.target_version ? ` -> ${status.target_version}` : "";
const error = status.error_message ? `, ошибка: ${status.error_message}` : "";
return `${status.current_version || "н/д"}${target}, ${status.phase}/${status.status}, ${formatDate(status.observed_at)}${error}`;
}
function vpnPacketStatLine(stats: VPNPacketStats[keyof VPNPacketStats] | undefined) {
if (!stats) {
return "нет данных";
}
return `push ${stats.pushed || 0} / pop ${stats.popped || 0} / q ${stats.queue_depth || 0} / drop ${stats.dropped || 0}`;
}
function updateStatusTone(status: NodeUpdateStatus | undefined) {
if (!status) {
return "warn";
}
const value = `${status.phase}:${status.status}`.toLowerCase();
if (value.includes("error") || value.includes("failed") || value.includes("rollback")) {
return "bad";
}
if (value.includes("success") || value.includes("updated") || value.includes("noop") || value.includes("already_current")) {
return "good";
}
if (value.includes("download") || value.includes("replace") || value.includes("plan") || value.includes("apply")) {
return "warn";
}
return "";
}
function serviceChannelFeedbackTone(status: string) {
const value = status.toLowerCase();
if (value === "healthy") {
return "good";
}
if (value === "fenced") {
return "bad";
}
if (value === "degraded") {
return "warn";
}
if (value === "operator_retry_cooldown") {
return "warn";
}
return "";
}
function serviceChannelRecoveryTone(status: string) {
const value = status.toLowerCase();
if (value === "healthy") {
return "good";
}
if (value === "recovered" || value === "cooldown" || value === "degraded") {
return "warn";
}
if (value === "fenced" || value === "demoted") {
return "bad";
}
return "";
}
function routeIntentLifecycle(item: MeshRouteIntent) {
if (item.status === "disabled" || item.lifecycle_status === "disabled") {
return "disabled";
}
if (item.is_expired || item.lifecycle_status === "expired") {
return "expired";
}
const expiresAt = Date.parse(item.policy_expires_at || "");
if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
return "expired";
}
return item.lifecycle_status || item.status || "active";
}
function routeIntentTone(item: MeshRouteIntent) {
const lifecycle = routeIntentLifecycle(item);
if (lifecycle === "active") {
return "good";
}
if (lifecycle === "expired") {
return "warn";
}
if (lifecycle === "disabled") {
return "";
}
return "warn";
}
function routeIntentNodeSelector(selector: Record<string, unknown>) {
const nodeID = typeof selector.node_id === "string" ? selector.node_id : "";
if (nodeID) {
return shortId(nodeID);
}
const nodeIDs = Array.isArray(selector.node_ids) ? selector.node_ids.filter((item): item is string => typeof item === "string") : [];
if (nodeIDs.length > 0) {
return nodeIDs.map(shortId).join(", ");
}
return "selector";
}
function nodeUpdaterSummary(statuses: NodeUpdateStatus[]) {
const nodeAgent = latestUpdateStatus(statuses, "rap-node-agent");
const hostAgent = latestUpdateStatus(statuses, "rap-host-agent");
if (!nodeAgent && !hostAgent) {
return { label: "updater: нет отчета", detail: "repair/update task не отчитался", tone: "bad" };
}
const stale = [nodeAgent, hostAgent].some((status) => status && isUpdateStatusStale(status));
const missingHost = !hostAgent;
const stagedHost = hostAgent?.phase === "apply" && hostAgent?.status === "staged";
const bad = [nodeAgent, hostAgent].some((status) => status && updateStatusTone(status) === "bad");
const nodeLabel = nodeAgent ? `${nodeAgent.current_version || "?"}->${nodeAgent.target_version || "?"}` : "node ?";
const hostLabel = hostAgent ? `${hostAgent.current_version || "?"}->${hostAgent.target_version || "?"}` : "host ?";
const observed = formatDate((hostAgent || nodeAgent)?.observed_at);
if (bad) {
return { label: "updater: ошибка", detail: `${nodeLabel}; ${hostLabel}; ${observed}`, tone: "bad" };
}
if (missingHost) {
return { label: "repair updater", detail: `host-agent не отчитался; ${nodeLabel}; ${observed}`, tone: "warn" };
}
if (stagedHost) {
return { label: "host-agent staged", detail: `${hostLabel}; нужен следующий запуск updater`, tone: "warn" };
}
if (stale) {
return { label: "updater: stale", detail: `${nodeLabel}; ${hostLabel}; ${observed}`, tone: "warn" };
}
return { label: "updater: ok", detail: `${nodeLabel}; ${hostLabel}; ${observed}`, tone: "good" };
}
function isUpdateStatusStale(status: NodeUpdateStatus) {
const observed = new Date(status.observed_at).getTime();
return !Number.isFinite(observed) || Date.now() - observed > 15 * 60 * 1000;
}
function joinTokenName(token: NodeJoinToken) {
const nodeName = typeof token.scope?.node_name === "string" ? token.scope.node_name : "";
const purpose = typeof token.scope?.purpose === "string" ? token.scope.purpose : "";
return nodeName || purpose || shortId(token.id);
}
function latestLinkObservations(links: MeshLink[]) {
const byPair = new Map<string, MeshLink>();
for (const link of links) {
const key = `${link.source_node_id}->${link.target_node_id}:${meshLinkObservationKey(link)}`;
const existing = byPair.get(key);
if (!existing || new Date(link.observed_at).getTime() > new Date(existing.observed_at).getTime()) {
byPair.set(key, link);
}
}
return [...byPair.values()];
}
function meshLinkObservationKey(link: MeshLink) {
const observationType = meshLinkMetadataString(link, "observation_type") || "default";
if (observationType === "synthetic_route_health") {
return `${observationType}:${meshLinkMetadataString(link, "route_id") || link.id}`;
}
if (observationType === "peer_connection_manager") {
return `${observationType}:${meshLinkMetadataString(link, "transport_mode")}:${meshLinkMetadataString(link, "relay_node_id")}`;
}
return observationType;
}
function meshLinkObservationLabel(link: MeshLink) {
const observationType = meshLinkMetadataString(link, "observation_type");
if (observationType === "synthetic_route_health") {
const drift = link.metadata?.route_path_drift_detected === true ? "drift" : "ok";
const applied = link.metadata?.route_path_decision_applied === true ? "decision" : "route";
return `route-health ${applied} ${drift}`;
}
if (observationType === "peer_connection_manager") {
const mode = meshLinkMetadataString(link, "transport_mode") || "manager";
const state = meshLinkMetadataString(link, "connection_state");
return state ? `${mode} ${state}` : mode;
}
return observationType || "link";
}
function meshLinkRoutePathSummary(link: MeshLink, nodes: ClusterNode[]) {
const routeID = meshLinkMetadataString(link, "route_id");
const relayID = meshLinkMetadataString(link, "route_path_decision_selected_relay_id") || meshLinkMetadataString(link, "relay_node_id");
const hops = meshLinkMetadataStringArray(link, "expected_effective_hops");
const observed = meshLinkMetadataStringArray(link, "observed_ack_path");
const path = hops.length > 0 ? hops : observed;
const parts: string[] = [];
if (routeID) {
parts.push(shortId(routeID));
}
if (relayID) {
parts.push(`via ${shortId(relayID)}`);
}
if (path.length > 0) {
parts.push(path.map((id) => trimNodeName(nodeName(nodes, id))).join(" > "));
}
return parts.length > 0 ? parts.join(" / ") : "н/д";
}
function meshLinkTopologyLabel(link: MeshLink, status: TopologyLinkStatus = link.link_status === "reachable" ? "reachable" : "unknown") {
if (status === "stale") {
return "stale";
}
if (status === "one_way") {
return "one-way";
}
if (meshLinkMetadataString(link, "observation_type") === "synthetic_route_health") {
return link.metadata?.route_path_drift_detected === true ? "drift" : "route";
}
return link.latency_ms == null ? "связь" : `${link.latency_ms}мс`;
}
function meshLinkMetadataString(link: MeshLink, key: string) {
const value = link.metadata?.[key];
return typeof value === "string" ? value : "";
}
function meshLinkMetadataStringArray(link: MeshLink, key: string) {
const value = link.metadata?.[key];
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
}
function objectField(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
}
function arrayObjects(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.map((item) => objectField(item)).filter((item): item is Record<string, unknown> => Boolean(item)) : [];
}
function stringField(source: Record<string, unknown> | undefined, key: string, fallback = "") {
const value = source?.[key];
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return fallback;
}
function firstStringField(source: Record<string, unknown> | undefined, keys: string[]) {
for (const key of keys) {
const value = stringField(source, key, "");
if (value) {
return value;
}
}
return "";
}
function nodeRuntimeConnectivity(node: ClusterNode, heartbeats: NodeHeartbeat[], meshLinks: MeshLink[]): NodeRuntimeConnectivity {
const heartbeat = heartbeats[0];
const metadata = heartbeat?.metadata || {};
const listenerReport = objectField(metadata.mesh_listener_report);
const endpointReport = objectField(metadata.mesh_endpoint_report);
const outboundReport = objectField(metadata.mesh_outbound_session_report);
const managerReport = objectField(metadata.mesh_peer_connection_manager_report);
const recoveryReport = objectField(metadata.mesh_peer_recovery_report);
const reportedCandidates = arrayObjects(endpointReport?.endpoint_candidates);
const firstCandidate = reportedCandidates[0];
const address =
firstStringField(endpointReport, ["peer_endpoint", "advertised_endpoint", "endpoint"]) ||
stringField(firstCandidate, "address", "") ||
stringField(listenerReport, "effective_listen_addr", "") ||
"адрес не прислан";
if (!heartbeat && !node.last_seen_at) {
return {
agentLabel: "agent: no heartbeat",
agentTone: "bad",
clientLabel: "client: unknown",
clientTone: "warn",
outboundLabel: "outbound: no heartbeat",
outboundTone: "bad",
inboundLabel: "inbound: unknown",
inboundTone: "warn",
address,
detail: "Узел создан/одобрен, но node-agent еще ни разу не прислал heartbeat.",
};
}
const readyPeers =
numberField(managerReport, "peer_connection_ready") ||
numberField(recoveryReport, "peer_connection_ready") ||
latestLinkObservations(meshLinks).filter((link) => (link.source_node_id === node.id || link.target_node_id === node.id) && link.link_status === "reachable").length;
const totalPeers =
numberField(managerReport, "peer_connection_total") ||
numberField(recoveryReport, "peer_connection_total") ||
latestLinkObservations(meshLinks).filter((link) => link.source_node_id === node.id || link.target_node_id === node.id).length;
const managerFailed = numberField(managerReport, "failed");
const listenerStatus = stringField(listenerReport, "status", "");
const portConflict = listenerReport?.port_conflict === true;
const oneWay =
listenerReport?.one_way_connectivity === true ||
stringField(endpointReport, "connectivity_mode", "") === "outbound_only" ||
numberField(managerReport, "peer_connection_relay_ready") > 0;
let inboundLabel = "inbound: no report";
let inboundTone = "warn";
if (listenerStatus === "listening" || listenerStatus === "auto_rebound") {
inboundLabel = listenerStatus === "auto_rebound" ? "inbound: auto port" : "inbound: listening";
inboundTone = "good";
} else if (listenerStatus === "listen_failed") {
inboundLabel = portConflict ? "inbound: port busy" : "inbound: failed";
inboundTone = "bad";
} else if (listenerStatus === "disabled") {
inboundLabel = "inbound: disabled";
inboundTone = oneWay ? "warn" : "bad";
} else if (endpointReport) {
inboundLabel = "inbound: advertised";
inboundTone = "good";
}
let clientLabel = "client: no peers";
let clientTone = "warn";
if (readyPeers > 0) {
clientLabel = `client: ready ${readyPeers}/${Math.max(totalPeers, readyPeers)}`;
clientTone = "good";
} else if (managerFailed > 0 || totalPeers > 0) {
clientLabel = `client: backoff ${readyPeers}/${Math.max(totalPeers, managerFailed)}`;
clientTone = "bad";
}
const outboundStatus = stringField(outboundReport, "status", "");
const usableForInboundControl = outboundReport?.usable_for_inbound_control === true;
const relayReady = numberField(outboundReport, "peer_connection_relay_ready");
const rendezvousLeases = numberField(outboundReport, "rendezvous_lease_count");
let outboundLabel = "outbound: no report";
let outboundTone = "warn";
if (outboundStatus === "ready") {
outboundLabel = usableForInboundControl ? "outbound: ready reverse" : "outbound: ready";
outboundTone = "good";
} else if (outboundStatus === "backoff" || outboundStatus === "failed") {
outboundLabel = `outbound: ${outboundStatus}`;
outboundTone = "bad";
} else if (oneWay || relayReady > 0 || rendezvousLeases > 0) {
outboundLabel = "outbound: inferred";
outboundTone = "warn";
}
const agentTone = node.health_status === "healthy" ? "good" : node.health_status === "unknown" ? "warn" : "bad";
return {
agentLabel: heartbeat ? "agent: heartbeat" : "agent: stale",
agentTone,
clientLabel: oneWay && readyPeers > 0 ? `${clientLabel} one-way` : clientLabel,
clientTone,
outboundLabel,
outboundTone,
inboundLabel,
inboundTone,
address,
detail: stringField(listenerReport, "failure_error", stringField(listenerReport, "failure_reason", "")),
};
}
function numberField(source: Record<string, unknown> | undefined, key: string, fallback = 0) {
const value = source?.[key];
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function compactJSON(value: unknown) {
if (value == null) {
return "н/д";
}
const raw = JSON.stringify(value);
return raw.length > 140 ? `${raw.slice(0, 137)}...` : raw;
}
function peerCandidateProbeSummary(result: Record<string, unknown>) {
const candidates = arrayObjects(result.candidate_results);
if (candidates.length === 0) {
return "н/д";
}
return candidates
.slice(0, 4)
.map((candidate) => {
const id = stringField(candidate, "candidate_id", "candidate");
const status = stringField(candidate, "link_status", "unknown");
const latency = stringField(candidate, "latency_ms", "");
return latency && latency !== "0" ? `${id}:${status}:${latency}мс` : `${id}:${status}`;
})
.join(", ");
}
function countPeerEndpointCandidates(config: NodeSyntheticMeshConfig) {
return Object.values(config.peer_endpoint_candidates || {}).reduce((sum, candidates) => sum + candidates.length, 0);
}
function rendezvousRelayPolicySummary(config?: NodeSyntheticMeshConfig) {
const policy = config?.rendezvous_relay_policy;
if (!policy) {
return "none";
}
const parts = [`stale${policy.stale_relay_count}`, `wd${policy.withdrawn_lease_count}`, `repl${policy.replacement_lease_count}`];
if (policy.scoring_mode.includes("synthetic_route_health_feedback")) {
parts.push("rh feedback");
}
const selected = policy.decisions?.find((decision) => decision.selected_relay_id);
if (selected?.selected_relay_id) {
parts.push(`via ${shortId(selected.selected_relay_id)}`);
}
return parts.join(" ");
}
function routePathDecisionSummary(config?: NodeSyntheticMeshConfig) {
const report = config?.route_path_decisions;
if (!report) {
return "none";
}
const parts = [`path${report.decision_count}`, `repl${report.replacement_decision_count}`];
if ((report.degraded_decision_count || 0) > 0) {
parts.push(`degr${report.degraded_decision_count}`);
}
const selected = report.decisions?.find((decision) => decision.selected_relay_id || decision.next_hop_id);
if (selected?.selected_relay_id) {
parts.push(`via ${shortId(selected.selected_relay_id)}`);
} else if (selected?.next_hop_id) {
parts.push(`next ${shortId(selected.next_hop_id)}`);
}
return parts.join(" ");
}
function nodeName(nodes: ClusterNode[], id: string) {
return nodes.find((node) => node.id === id)?.name || shortId(id);
}
function groupPathLabel(group: ClusterNodeGroup, groups: ClusterNodeGroup[]) {
const byId = new Map(groups.map((item) => [item.id, item]));
const parts = [group.name];
let parentID = group.parent_group_id;
const seen = new Set<string>([group.id]);
while (parentID && !seen.has(parentID)) {
seen.add(parentID);
const parent = byId.get(parentID);
if (!parent) {
break;
}
parts.unshift(parent.name);
parentID = parent.parent_group_id;
}
return parts.join(" / ");
}
function groupPathLabelById(groupId: string, groups: ClusterNodeGroup[]) {
const group = groups.find((item) => item.id === groupId);
return group ? groupPathLabel(group, groups) : groupId;
}
function descendantGroupIds(groupId: string, groups: ClusterNodeGroup[]) {
const result: string[] = [];
const childrenByParent = new Map<string, ClusterNodeGroup[]>();
for (const group of groups) {
const parentKey = group.parent_group_id || "";
childrenByParent.set(parentKey, [...(childrenByParent.get(parentKey) || []), group]);
}
const visit = (parentId: string) => {
for (const child of childrenByParent.get(parentId) || []) {
result.push(child.id);
visit(child.id);
}
};
visit(groupId);
return result;
}
function buildNodeInventoryTreeRows(
entries: NodeInventoryEntry[],
groups: ClusterNodeGroup[],
selectedClusterId: string,
labels: Record<string, string>,
collapsedGroupKeys: Set<string>,
): NodeInventoryTreeRow[] {
const rows: NodeInventoryTreeRow[] = [];
const entriesByGroup = new Map<string, NodeInventoryEntry[]>();
const activeUngrouped: NodeInventoryEntry[] = [];
const outsideActiveCluster: NodeInventoryEntry[] = [];
for (const entry of entries) {
const activeMembership = entry.memberships.find((membership) => membership.cluster.id === selectedClusterId);
if (!activeMembership) {
outsideActiveCluster.push(entry);
continue;
}
const groupID = activeMembership.node.node_group_id;
if (!groupID) {
activeUngrouped.push(entry);
continue;
}
entriesByGroup.set(groupID, [...(entriesByGroup.get(groupID) || []), entry]);
}
const childrenByParent = new Map<string, ClusterNodeGroup[]>();
for (const group of groups) {
const parentKey = group.parent_group_id || "";
childrenByParent.set(parentKey, [...(childrenByParent.get(parentKey) || []), group]);
}
for (const childGroups of childrenByParent.values()) {
childGroups.sort((left, right) => left.sort_order - right.sort_order || left.name.localeCompare(right.name));
}
const countByGroup = new Map<string, number>();
const countGroupEntries = (group: ClusterNodeGroup): number => {
const cached = countByGroup.get(group.id);
if (cached != null) {
return cached;
}
let count = entriesByGroup.get(group.id)?.length || 0;
for (const child of childrenByParent.get(group.id) || []) {
count += countGroupEntries(child);
}
countByGroup.set(group.id, count);
return count;
};
const appendGroup = (group: ClusterNodeGroup, depth: number): number => {
const ownEntries = [...(entriesByGroup.get(group.id) || [])].sort((left, right) => left.node.name.localeCompare(right.node.name));
const childGroups = childrenByParent.get(group.id) || [];
const rowKey = `group-${group.id}`;
const count = countGroupEntries(group);
rows.push({ kind: "group", key: rowKey, label: group.name, depth, count, groupId: group.id });
if (!collapsedGroupKeys.has(rowKey)) {
for (const entry of ownEntries) {
rows.push({ kind: "node", key: `node-${group.id}-${entry.node.id}`, entry, depth: depth + 1 });
}
for (const child of childGroups) {
appendGroup(child, depth + 1);
}
}
return count;
};
for (const group of childrenByParent.get("") || []) {
appendGroup(group, 0);
}
if (activeUngrouped.length > 0) {
const rowKey = "group-ungrouped";
rows.push({ kind: "group", key: rowKey, label: labels.ungroupedNodes, depth: 0, count: activeUngrouped.length });
if (!collapsedGroupKeys.has(rowKey)) {
for (const entry of activeUngrouped.sort((left, right) => left.node.name.localeCompare(right.node.name))) {
rows.push({ kind: "node", key: `node-ungrouped-${entry.node.id}`, entry, depth: 1 });
}
}
}
if (outsideActiveCluster.length > 0) {
const rowKey = "group-outside-active-cluster";
rows.push({
kind: "group",
key: rowKey,
label: labels.notMemberOfActiveCluster,
depth: 0,
count: outsideActiveCluster.length,
});
if (!collapsedGroupKeys.has(rowKey)) {
for (const entry of outsideActiveCluster.sort((left, right) => left.node.name.localeCompare(right.node.name))) {
rows.push({ kind: "node", key: `node-outside-${entry.node.id}`, entry, depth: 1 });
}
}
}
return rows;
}
function nodesWithRole(role: string, rolesByNode: Record<string, RoleAssignment[]>) {
return Object.entries(rolesByNode)
.filter(([, roles]) => roles.some((assignment) => assignment.role === role && assignment.status === "active"))
.map(([nodeId]) => nodeId);
}
function buildJoinTokenScope(input: JoinTokenFormState): Record<string, unknown> {
const scope: Record<string, unknown> = {
roles: input.roles,
node_name: input.nodeName.trim() || null,
node_group_id: input.nodeGroupId || null,
ownership_type: input.ownershipType,
purpose: input.purpose.trim() || null,
approval: {
mode: "manual",
auto_approve: false,
role_assignment: "manual_after_approval",
},
source: "platform_owner_console",
};
if (input.installMode === "docker") {
const endpoint = (input.controlPlaneEndpoint || defaultControlPlaneEndpoint()).trim().replace(/\/$/, "");
scope.install_profile = "docker";
scope.backend_url = endpoint;
scope.control_plane_endpoints = [endpoint];
scope.image = input.dockerImage || "rap-node-agent:latest";
if (input.dockerContainerName.trim()) {
scope.container_name = input.dockerContainerName.trim();
}
scope.artifact_endpoints = csvValues(input.artifactEndpoints || defaultArtifactEndpoint());
if (input.dockerImageArtifactSHA256.trim()) {
scope.docker_image_artifact_sha256 = input.dockerImageArtifactSHA256.trim();
}
scope.network = input.dockerNetwork || "host";
scope.restart_policy = "unless-stopped";
scope.pull_image = Boolean(input.pullImage);
scope.replace = input.replace !== false;
scope.mesh_synthetic_runtime_enabled = input.syntheticRuntime !== false;
scope.mesh_production_forwarding_enabled = false;
scope.mesh_listen_addr = input.meshListenAddr || ":19131";
scope.mesh_listen_port_mode = input.meshListenPortMode || "auto";
scope.mesh_listen_auto_port_start = input.meshListenAutoPortStart || 19131;
scope.mesh_listen_auto_port_end = input.meshListenAutoPortEnd || 19231;
if (input.meshAdvertiseEndpoint?.trim()) {
scope.mesh_advertise_endpoint = input.meshAdvertiseEndpoint.trim().replace(/\/$/, "");
}
scope.mesh_advertise_transport = input.meshAdvertiseTransport || "direct_http";
scope.mesh_connectivity_mode = input.meshConnectivityMode || "private_lan";
scope.mesh_nat_type = input.meshNATType || "unknown";
scope.mesh_region = input.meshRegion || null;
}
if (input.installMode === "windows_service") {
const endpoint = (input.controlPlaneEndpoint || defaultControlPlaneEndpoint()).trim().replace(/\/$/, "");
scope.install_profile = "windows_service";
scope.backend_url = endpoint;
scope.control_plane_endpoints = [endpoint];
scope.artifact_endpoints = csvValues(input.artifactEndpoints || defaultArtifactEndpoint());
scope.startup_mode = input.windowsStartupMode || "auto";
if (input.windowsInstallDir.trim()) {
scope.install_dir = input.windowsInstallDir.trim();
}
if (input.windowsNodeAgentSHA256.trim()) {
scope.node_agent_artifact_sha256 = input.windowsNodeAgentSHA256.trim();
}
scope.mesh_synthetic_runtime_enabled = input.syntheticRuntime !== false;
scope.mesh_production_forwarding_enabled = false;
scope.mesh_listen_addr = input.meshListenAddr || ":19131";
scope.mesh_listen_port_mode = input.meshListenPortMode || "auto";
scope.mesh_listen_auto_port_start = input.meshListenAutoPortStart || 19131;
scope.mesh_listen_auto_port_end = input.meshListenAutoPortEnd || 19231;
if (input.meshAdvertiseEndpoint?.trim()) {
scope.mesh_advertise_endpoint = input.meshAdvertiseEndpoint.trim().replace(/\/$/, "");
}
scope.mesh_advertise_transport = input.meshAdvertiseTransport || "direct_http";
scope.mesh_connectivity_mode = input.meshConnectivityMode || "outbound_only";
scope.mesh_nat_type = input.meshNATType || "unknown";
scope.mesh_region = input.meshRegion || "windows";
}
if (input.installMode === "linux_binary") {
const endpoint = (input.controlPlaneEndpoint || defaultControlPlaneEndpoint()).trim().replace(/\/$/, "");
scope.install_profile = "linux_binary";
scope.backend_url = endpoint;
scope.control_plane_endpoints = [endpoint];
scope.artifact_endpoints = csvValues(input.artifactEndpoints || defaultArtifactEndpoint());
scope.startup_mode = "systemd";
if (input.linuxInstallDir.trim()) {
scope.install_dir = input.linuxInstallDir.trim();
}
if (input.linuxNodeAgentSHA256.trim()) {
scope.node_agent_artifact_sha256 = input.linuxNodeAgentSHA256.trim();
}
scope.replace = input.replace !== false;
scope.mesh_synthetic_runtime_enabled = input.syntheticRuntime !== false;
scope.mesh_production_forwarding_enabled = false;
scope.mesh_listen_addr = input.meshListenAddr || ":19131";
scope.mesh_listen_port_mode = input.meshListenPortMode || "auto";
scope.mesh_listen_auto_port_start = input.meshListenAutoPortStart || 19131;
scope.mesh_listen_auto_port_end = input.meshListenAutoPortEnd || 19231;
if (input.meshAdvertiseEndpoint?.trim()) {
scope.mesh_advertise_endpoint = input.meshAdvertiseEndpoint.trim().replace(/\/$/, "");
}
scope.mesh_advertise_transport = input.meshAdvertiseTransport || "direct_http";
scope.mesh_connectivity_mode = input.meshConnectivityMode || "outbound_only";
scope.mesh_nat_type = input.meshNATType || "unknown";
scope.mesh_region = input.meshRegion || "linux";
}
return scope;
}
function joinTokenFormFromScope(scope: Record<string, unknown>, fallback: JoinTokenFormState): JoinTokenFormState {
const roles = arrayStrings(scope.roles);
const artifactEndpoints = arrayStrings(scope.artifact_endpoints).join(", ");
return {
...fallback,
roles: roles.length > 0 ? roles : fallback.roles,
nodeName: stringField(scope, "node_name", "") || fallback.nodeName,
nodeGroupId: stringField(scope, "node_group_id", "") || fallback.nodeGroupId,
ownershipType: stringField(scope, "ownership_type", fallback.ownershipType),
purpose: stringField(scope, "purpose", "") || fallback.purpose,
installMode: stringField(scope, "install_profile", fallback.installMode),
dockerImage: stringField(scope, "image", fallback.dockerImage),
dockerContainerName: stringField(scope, "container_name", "") || fallback.dockerContainerName,
dockerNetwork: stringField(scope, "network", fallback.dockerNetwork),
windowsStartupMode: stringField(scope, "startup_mode", fallback.windowsStartupMode),
windowsInstallDir: stringField(scope, "install_dir", "") || fallback.windowsInstallDir,
windowsNodeAgentSHA256: stringField(scope, "node_agent_artifact_sha256", "") || fallback.windowsNodeAgentSHA256,
linuxInstallDir: stringField(scope, "install_dir", "") || fallback.linuxInstallDir,
linuxNodeAgentSHA256: stringField(scope, "node_agent_artifact_sha256", "") || fallback.linuxNodeAgentSHA256,
meshListenAddr: stringField(scope, "mesh_listen_addr", fallback.meshListenAddr),
meshListenPortMode: stringField(scope, "mesh_listen_port_mode", fallback.meshListenPortMode),
meshListenAutoPortStart: numberField(scope, "mesh_listen_auto_port_start", fallback.meshListenAutoPortStart),
meshListenAutoPortEnd: numberField(scope, "mesh_listen_auto_port_end", fallback.meshListenAutoPortEnd),
meshAdvertiseEndpoint: stringField(scope, "mesh_advertise_endpoint", "") || fallback.meshAdvertiseEndpoint,
meshAdvertiseTransport: stringField(scope, "mesh_advertise_transport", fallback.meshAdvertiseTransport),
meshConnectivityMode: stringField(scope, "mesh_connectivity_mode", fallback.meshConnectivityMode),
meshNATType: stringField(scope, "mesh_nat_type", fallback.meshNATType),
meshRegion: stringField(scope, "mesh_region", "") || fallback.meshRegion,
controlPlaneEndpoint: arrayStrings(scope.control_plane_endpoints)[0] || stringField(scope, "backend_url", "") || fallback.controlPlaneEndpoint,
artifactEndpoints: artifactEndpoints || fallback.artifactEndpoints,
dockerImageArtifactSHA256: stringField(scope, "docker_image_artifact_sha256", "") || fallback.dockerImageArtifactSHA256,
pullImage: booleanField(scope, "pull_image", fallback.pullImage),
replace: booleanField(scope, "replace", fallback.replace),
syntheticRuntime: booleanField(scope, "mesh_synthetic_runtime_enabled", fallback.syntheticRuntime),
};
}
function arrayStrings(value: unknown) {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
}
function booleanField(source: Record<string, unknown>, key: string, fallback: boolean) {
const value = source[key];
return typeof value === "boolean" ? value : fallback;
}
function defaultControlPlaneEndpoint() {
if (typeof window === "undefined" || !window.location?.origin) {
return "http://<control-plane-host>:18080/api/v1";
}
return `${window.location.origin.replace(/\/$/, "")}/api/v1`;
}
function defaultArtifactEndpoint() {
if (typeof window === "undefined" || !window.location?.origin) {
return "http://<bootstrap-host>:18080/downloads";
}
return `${window.location.origin.replace(/\/$/, "")}/downloads`;
}
function csvValues(value: string) {
return value
.split(",")
.map((item) => item.trim().replace(/\/$/, ""))
.filter(Boolean);
}
function dockerImageArtifactUrls(form: JoinTokenFormState) {
const fileName = "rap-node-agent-dev-enrollment-bootstrap-smoke.tar";
return csvValues(form.artifactEndpoints || defaultArtifactEndpoint()).map((endpoint) => `${endpoint}/${fileName}`);
}
function dockerConnectivityPreset(input: { meshConnectivityMode: string; meshNATType: string; meshAdvertiseEndpoint: string }) {
if (input.meshConnectivityMode === "outbound_only") {
return "outbound_only";
}
if (input.meshConnectivityMode === "private_lan") {
return "private_lan";
}
if (input.meshNATType !== "none" && input.meshAdvertiseEndpoint.trim()) {
return "nat_forward";
}
return "direct";
}
function applyDockerConnectivityPreset(input: JoinTokenFormState, mode: string): JoinTokenFormState {
const next = { ...input };
if (mode === "private_lan") {
next.meshConnectivityMode = "private_lan";
next.meshNATType = "none";
} else if (mode === "direct") {
next.meshConnectivityMode = "direct";
next.meshNATType = "none";
} else if (mode === "nat_forward") {
next.meshConnectivityMode = "direct";
next.meshNATType = "port_restricted";
} else {
next.meshConnectivityMode = "outbound_only";
next.meshNATType = "symmetric";
next.meshAdvertiseEndpoint = "";
}
return next;
}
function suggestedInstallNodeName(form: Pick<JoinTokenFormState, "nodeName">, cluster: Cluster | null) {
if (form.nodeName.trim()) {
return form.nodeName.trim();
}
const slug = safeShellSlug(cluster?.slug || cluster?.name || "rap-node");
return `${slug}-node-1`;
}
function suggestedInstallContainerName(form: Pick<JoinTokenFormState, "nodeName" | "dockerContainerName">, cluster: Cluster | null) {
if (form.dockerContainerName.trim()) {
return form.dockerContainerName.trim();
}
return `rap-node-agent-${safeShellSlug(suggestedInstallNodeName(form, cluster))}`;
}
function hostAgentDockerInstallCommand(token: CreatedJoinToken, cluster: Cluster | null, form: JoinTokenFormState = defaultJoinTokenForm) {
const clusterID = cluster?.id || token.cluster_id;
const nodeName = suggestedInstallNodeName(form, cluster);
const containerName = suggestedInstallContainerName(form, cluster);
const slug = safeShellSlug(nodeName);
const endpoint = installControlPlaneEndpoint(form);
const command = [
"rap-host-agent install",
`--backend-url ${shellQuote(endpoint)}`,
`--cluster-id ${shellQuote(clusterID)}`,
`--join-token ${shellQuote(token.token)}`,
`--node-name ${shellQuote(nodeName)}`,
`--image ${shellQuote(form.dockerImage || "rap-node-agent:latest")}`,
`--container-name ${shellQuote(containerName)}`,
`--state-dir ${shellQuote(`/var/lib/rap/nodes/${slug}`)}`,
"--network host",
"--replace",
];
for (const artifactURL of dockerImageArtifactUrls(form)) {
command.push(`--image-artifact-url ${shellQuote(artifactURL)}`);
}
if (form.dockerImageArtifactSHA256.trim()) {
command.push(`--image-artifact-sha256 ${shellQuote(form.dockerImageArtifactSHA256.trim())}`);
}
return command.join(" \\\n ");
}
function hostAgentDockerProfileInstallCommand(token: CreatedJoinToken, cluster: Cluster | null, form: JoinTokenFormState = defaultJoinTokenForm) {
const clusterID = cluster?.id || token.cluster_id;
const nodeName = suggestedInstallNodeName(form, cluster);
const endpoint = installControlPlaneEndpoint(form);
const installCommand = [
'sudo "$rap_host_agent" install',
`--profile-url ${shellQuote(endpoint)}`,
`--cluster-id ${shellQuote(clusterID)}`,
`--install-token ${shellQuote(token.token)}`,
`--node-name ${shellQuote(nodeName)}`,
].join(" \\\n ");
return [
'rap_host_agent="$(mktemp /tmp/rap-host-agent.XXXXXX)"',
`curl -fL --retry 3 --retry-delay 1 ${shellQuote(hostAgentDownloadUrl(form))} -o "$rap_host_agent"`,
'chmod +x "$rap_host_agent"',
installCommand,
].join(" && \\\n ");
}
function hostAgentLinuxProfileInstallCommand(token: CreatedJoinToken, cluster: Cluster | null, form: JoinTokenFormState = defaultJoinTokenForm) {
const clusterID = cluster?.id || token.cluster_id;
const nodeName = suggestedInstallNodeName(form, cluster);
const endpoint = installControlPlaneEndpoint(form);
const installCommand = [
'sudo "$rap_host_agent" install-linux',
`--profile-url ${shellQuote(endpoint)}`,
`--cluster-id ${shellQuote(clusterID)}`,
`--install-token ${shellQuote(token.token)}`,
`--node-name ${shellQuote(nodeName)}`,
].join(" \\\n ");
return [
'rap_host_agent="$(mktemp /tmp/rap-host-agent.XXXXXX)"',
`curl -fL --retry 3 --retry-delay 1 ${shellQuote(hostAgentDownloadUrl(form))} -o "$rap_host_agent"`,
'chmod +x "$rap_host_agent"',
installCommand,
].join(" && \\\n ");
}
function hostAgentWindowsProfileInstallCommand(token: CreatedJoinToken, cluster: Cluster | null, form: JoinTokenFormState = defaultJoinTokenForm) {
const clusterID = cluster?.id || token.cluster_id;
const nodeName = suggestedInstallNodeName(form, cluster);
const endpoint = installControlPlaneEndpoint(form);
const hostAgentUrl = hostAgentWindowsDownloadUrl(form);
return [
`$rapHostAgent = Join-Path $env:TEMP "rap-host-agent.exe"`,
`Invoke-WebRequest -UseBasicParsing ${powershellQuote(hostAgentUrl)} -OutFile $rapHostAgent`,
`& $rapHostAgent install-windows --profile-url ${powershellQuote(endpoint)} --cluster-id ${powershellQuote(clusterID)} --install-token ${powershellQuote(token.token)} --node-name ${powershellQuote(nodeName)} --startup-mode ${powershellQuote(form.windowsStartupMode || "auto")}`,
].join("\r\n");
}
function hostAgentWindowsCmdProfileInstallCommand(token: CreatedJoinToken, cluster: Cluster | null, form: JoinTokenFormState = defaultJoinTokenForm) {
const clusterID = cluster?.id || token.cluster_id;
const nodeName = suggestedInstallNodeName(form, cluster);
const endpoint = installControlPlaneEndpoint(form);
const hostAgentUrl = hostAgentWindowsDownloadUrl(form);
const startupMode = form.windowsStartupMode || "auto";
return [
`powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest -UseBasicParsing '${hostAgentUrl}' -OutFile $env:TEMP\\rap-host-agent.exe"`,
`%TEMP%\\rap-host-agent.exe install-windows --profile-url "${endpoint}" --cluster-id "${clusterID}" --install-token "${token.token}" --node-name "${nodeName}" --startup-mode "${startupMode}"`,
].join("\r\n");
}
function windowsRepairUpdaterCmdCommand(node: ClusterNode, clusterID: string) {
const endpoint = externalPreferredControlPlaneEndpoint();
const origin = endpoint.replace(/\/api\/v1$/i, "").replace(/\/api$/i, "").replace(/\/$/, "");
const nodeName = node.name || node.node_key || node.id;
const nodeSlug = safeWindowsNodeSlug(nodeName);
const installDir = `%ProgramFiles%\\RAP\\${nodeSlug}`;
const stateDir = `%ProgramData%\\RAP\\nodes\\${nodeSlug}`;
const taskName = `RAP Node Agent ${nodeSlug}`;
const updaterTaskName = `RAP Host Agent Updater ${nodeSlug}`;
const hostAgentPath = `${installDir}\\rap-host-agent.exe`;
const stagedHostAgentPath = `${hostAgentPath}.next`;
return [
`@echo off`,
`echo === RAP Windows updater repair: ${cmdEscape(nodeName)} ===`,
`echo Node ID: ${node.id}`,
`echo Control Plane: ${endpoint}`,
`echo.`,
`echo === Before repair: scheduled tasks ===`,
`schtasks /Query /TN "${taskName}" /V /FO LIST`,
`schtasks /Query /TN "${updaterTaskName}" /V /FO LIST`,
`echo.`,
`echo === Before repair: binaries ===`,
`dir "${installDir}\\rap-*.exe*"`,
`echo.`,
`powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest -UseBasicParsing '${origin}/downloads/rap-host-agent-windows-amd64.exe' -OutFile $env:TEMP\\rap-host-agent.exe"`,
`%TEMP%\\rap-host-agent.exe install-windows --backend-url "${endpoint}" --cluster-id "${clusterID || "<cluster-id>"}" --node-id "${node.id}" --node-name "${cmdEscape(nodeName)}" --replace --startup-mode "auto" --auto-update-current-version "0.0.0" --auto-update-initial-delay-seconds 1`,
`"${hostAgentPath}" update-loop --backend-url "${endpoint}" --cluster-id "${clusterID || "<cluster-id>"}" --node-id "${node.id}" --state-dir "${stateDir}" --current-version "0.0.0" --os windows --arch amd64 --install-type windows_service --binary-path "${installDir}\\rap-node-agent.exe" --windows-task-name "${taskName}" --health-timeout-seconds 30 --interval-seconds 0 --initial-delay-seconds 0 --max-runs 1 --host-agent-update-status-enabled --host-agent-current-version "0.0.0" --host-agent-binary-path "${hostAgentPath}"`,
`echo.`,
`echo === Applying staged host-agent if present ===`,
`if exist "${stagedHostAgentPath}" copy /Y "${stagedHostAgentPath}" "${hostAgentPath}"`,
`if exist "${stagedHostAgentPath}" del /F /Q "${stagedHostAgentPath}"`,
`schtasks /End /TN "${updaterTaskName}"`,
`schtasks /Run /TN "${updaterTaskName}"`,
`echo.`,
`echo === After repair: binaries ===`,
`dir "${installDir}\\rap-*.exe*"`,
`echo.`,
`echo === After repair: updater task ===`,
`schtasks /Query /TN "${updaterTaskName}" /V /FO LIST`,
`echo.`,
`echo Repair command finished. Check the admin panel for rap-node-agent and rap-host-agent plan/noop reports.`,
].join("\r\n");
}
function windowsRepairUpdaterCmdFileName(node: ClusterNode) {
const nodeName = safeDownloadName(node.name || node.node_key || node.id || "node");
return `rap-repair-updater-${nodeName}.cmd`;
}
function linuxRepairUpdaterShCommand(node: ClusterNode, clusterID: string) {
const endpoint = externalPreferredControlPlaneEndpoint();
const origin = endpoint.replace(/\/api\/v1$/i, "").replace(/\/api$/i, "").replace(/\/$/, "");
const nodeName = node.name || node.node_key || node.id;
const nodeSlug = safeLinuxNodeSlug(nodeName);
const installDir = `/opt/rap/${nodeSlug}`;
const stateDir = `/var/lib/rap/nodes/${nodeSlug}`;
const unitName = `rap-node-agent-${nodeSlug}.service`;
const updaterUnitName = `rap-host-agent-updater-${nodeSlug}.service`;
const hostAgentPath = `${installDir}/rap-host-agent`;
return [
`#!/usr/bin/env bash`,
`set -euo pipefail`,
`echo "=== RAP Linux updater repair: ${shDoubleQuote(nodeName)} ==="`,
`echo "Node ID: ${node.id}"`,
`echo "Control Plane: ${endpoint}"`,
`echo`,
`echo "=== Before repair: systemd units ==="`,
`systemctl status ${shellQuote(unitName)} --no-pager || true`,
`systemctl status ${shellQuote(updaterUnitName)} --no-pager || true`,
`echo`,
`echo "=== Before repair: binaries ==="`,
`ls -la ${shellQuote(installDir)} || true`,
`echo`,
`rap_host_agent="$(mktemp /tmp/rap-host-agent.XXXXXX)"`,
`curl -fL --retry 3 --retry-delay 1 ${shellQuote(`${origin}/downloads/rap-host-agent-linux-amd64`)} -o "$rap_host_agent"`,
`chmod +x "$rap_host_agent"`,
`sudo "$rap_host_agent" install-linux --backend-url ${shellQuote(endpoint)} --cluster-id ${shellQuote(clusterID || "<cluster-id>")} --node-id ${shellQuote(node.id)} --node-name ${shellQuote(nodeName)} --replace --startup-mode systemd --auto-update-current-version 0.0.0 --auto-update-initial-delay-seconds 1`,
`sudo ${shellQuote(hostAgentPath)} update-loop --backend-url ${shellQuote(endpoint)} --cluster-id ${shellQuote(clusterID || "<cluster-id>")} --node-id ${shellQuote(node.id)} --state-dir ${shellQuote(stateDir)} --current-version 0.0.0 --os linux --arch amd64 --install-type linux_binary --binary-path ${shellQuote(`${installDir}/rap-node-agent`)} --systemd-unit ${shellQuote(unitName)} --health-timeout-seconds 30 --interval-seconds 0 --initial-delay-seconds 0 --max-runs 1 --host-agent-update-status-enabled --host-agent-current-version 0.0.0 --host-agent-binary-path ${shellQuote(hostAgentPath)}`,
`sudo systemctl daemon-reload`,
`sudo systemctl restart ${shellQuote(updaterUnitName)}`,
`echo`,
`echo "=== After repair: systemd updater ==="`,
`systemctl status ${shellQuote(updaterUnitName)} --no-pager || true`,
`echo "Repair command finished. Check the admin panel for rap-node-agent and rap-host-agent plan/noop reports."`,
].join("\n");
}
function linuxRepairUpdaterShFileName(node: ClusterNode) {
const nodeName = safeDownloadName(node.name || node.node_key || node.id || "node");
return `rap-repair-updater-${nodeName}.sh`;
}
function downloadTextFile(fileName: string, content: string) {
if (typeof document === "undefined") {
return;
}
const blob = new Blob([content.endsWith("\r\n") ? content : `${content}\r\n`], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
}
async function copyTextToClipboard(content: string) {
await navigator.clipboard.writeText(content);
}
function safeDownloadName(value: string) {
return value
.trim()
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 80) || "node";
}
function safeWindowsNodeSlug(value: string) {
return value
.trim()
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "") || "node";
}
function safeLinuxNodeSlug(value: string) {
return safeWindowsNodeSlug(value).slice(0, 48) || "node";
}
function shDoubleQuote(value: string) {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
}
function installControlPlaneEndpoint(form: JoinTokenFormState = defaultJoinTokenForm) {
return (form.controlPlaneEndpoint || defaultControlPlaneEndpoint()).trim().replace(/\/$/, "");
}
function externalPreferredControlPlaneEndpoint() {
const origin = typeof window === "undefined" ? "" : window.location?.origin || "";
if (/^(http:\/\/)?(192\.168\.200\.61|docker-test\.cin\.su)(:18080)?$/i.test(origin.replace(/\/$/, ""))) {
return "https://vpn.cin.su/api/v1";
}
return `${origin.replace(/\/$/, "")}/api/v1`;
}
function installArtifactOrigin(form: JoinTokenFormState = defaultJoinTokenForm) {
const firstArtifactEndpoint = csvValues(form.artifactEndpoints)[0];
if (firstArtifactEndpoint) {
return firstArtifactEndpoint.replace(/\/downloads$/i, "").replace(/\/$/, "");
}
const endpoint = installControlPlaneEndpoint(form);
return endpoint.replace(/\/api\/v1$/i, "").replace(/\/api$/i, "").replace(/\/$/, "");
}
function hostAgentDownloadUrl(form: JoinTokenFormState = defaultJoinTokenForm) {
const origin =
typeof window === "undefined" && !form.controlPlaneEndpoint
? "http://<control-plane-host>:18080"
: installArtifactOrigin(form);
return `${origin}/downloads/rap-host-agent-linux-amd64`;
}
function hostAgentWindowsDownloadUrl(form: JoinTokenFormState = defaultJoinTokenForm) {
const origin =
typeof window === "undefined" && !form.controlPlaneEndpoint
? "http://<control-plane-host>:18080"
: installArtifactOrigin(form);
return `${origin}/downloads/rap-host-agent-windows-amd64.exe`;
}
function safeShellSlug(value: string) {
return (
value
.trim()
.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 42) || "rap-node"
);
}
function shellQuote(value: string) {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function powershellQuote(value: string) {
return `'${value.replace(/'/g, "''")}'`;
}
function cmdEscape(value: string) {
return value.replace(/"/g, `""`);
}
function toggleArrayValue(values: string[], value: string) {
return values.includes(value) ? values.filter((item) => item !== value) : [...values, value];
}
function groupNodeInventory(
entries: NodeInventoryEntry[],
selectedClusterId: string,
search: string,
groupBy: "membership" | "health" | "ownership" | "cluster_count",
language: Language,
) {
const normalizedSearch = search.trim().toLowerCase();
const groups = new Map<string, NodeInventoryEntry[]>();
for (const entry of entries) {
if (normalizedSearch && !nodeInventoryMatches(entry, normalizedSearch)) {
continue;
}
const key = nodeInventoryGroupLabel(entry, selectedClusterId, groupBy, language);
groups.set(key, [...(groups.get(key) || []), entry]);
}
return Array.from(groups.entries())
.map(([label, items]) => ({ label, items: items.sort((left, right) => left.node.name.localeCompare(right.node.name)) }))
.sort((left, right) => left.label.localeCompare(right.label));
}
function nodeInventoryMatches(entry: NodeInventoryEntry, search: string) {
const values = [
entry.node.name,
entry.node.node_key,
entry.node.health_status,
entry.node.ownership_type,
entry.node.reported_version || "",
...entry.memberships.flatMap((membership) => [membership.cluster.name, membership.cluster.slug, membership.node.membership_status]),
];
return values.some((value) => value.toLowerCase().includes(search));
}
function nodeInventoryGroupLabel(
entry: NodeInventoryEntry,
selectedClusterId: string,
groupBy: "membership" | "health" | "ownership" | "cluster_count",
language: Language,
) {
if (groupBy === "health") {
return statusLabel(entry.node.health_status);
}
if (groupBy === "ownership") {
return statusLabel(entry.node.ownership_type);
}
if (groupBy === "cluster_count") {
return clusterCountLabel(entry.memberships.length, language);
}
const activeMembership = entry.memberships.find((membership) => membership.cluster.id === selectedClusterId);
if (!activeMembership) {
return language === "en" ? "Not in active cluster" : "Не в активном кластере";
}
if (activeMembership.node.membership_status === "active") {
return language === "en" ? "In active cluster" : "В активном кластере";
}
return `${language === "en" ? "Membership" : "Участие"}: ${statusLabel(activeMembership.node.membership_status)}`;
}
function capabilityState(role: string, heartbeat?: NodeHeartbeat) {
const expectedKeys = capabilityKeysByRole[role] || [];
if (expectedKeys.length === 0 || !heartbeat) {
return "unknown";
}
if (isHeartbeatStale(heartbeat)) {
return "stale";
}
const capabilities = heartbeat.capabilities || {};
return expectedKeys.some((key) => Boolean(capabilities[key])) ? "confirmed" : "missing";
}
function isHeartbeatStale(heartbeat?: NodeHeartbeat) {
if (!heartbeat?.observed_at) {
return true;
}
const observedAt = new Date(heartbeat.observed_at).getTime();
return !Number.isFinite(observedAt) || Date.now() - observedAt > 60 * 1000;
}
function capabilityPillTone(role: string, heartbeat?: NodeHeartbeat) {
const state = capabilityState(role, heartbeat);
if (state === "confirmed") {
return "good";
}
if (state === "missing") {
return "bad";
}
if (state === "stale") {
return "warn";
}
return "";
}
function capabilityLabel(role: string, heartbeat: NodeHeartbeat | undefined, labels: Record<string, string>) {
const state = capabilityState(role, heartbeat);
if (state === "confirmed") {
return labels.capabilityConfirmed;
}
if (state === "missing") {
return labels.capabilityMissing;
}
if (state === "stale") {
return "heartbeat устарел";
}
return labels.capabilityUnknown;
}
function capabilityOptionLabel(role: string, heartbeat: NodeHeartbeat | undefined, language: Language) {
const state = capabilityState(role, heartbeat);
if (language === "en") {
return state === "confirmed" ? "capable" : state === "missing" ? "not reported" : state === "stale" ? "stale heartbeat" : "unknown";
}
return state === "confirmed" ? "подходит" : state === "missing" ? "не заявлено" : state === "stale" ? "heartbeat устарел" : "неизвестно";
}
function meshRecoverySummary(heartbeat?: NodeHeartbeat) {
const report = heartbeat?.metadata?.mesh_peer_recovery_report;
if (!report || typeof report !== "object" || Array.isArray(report)) {
return "н/д";
}
const fields = report as Record<string, unknown>;
const mode = typeof fields.mode === "string" ? fields.mode : "unknown";
const ready = typeof fields.ready_peer_count === "number" ? fields.ready_peer_count : null;
const target = typeof fields.target_ready_peers === "number" ? fields.target_ready_peers : null;
const deficit = typeof fields.deficit === "number" ? fields.deficit : 0;
const base = ready == null || target == null ? mode : `${mode} ${ready}/${target}`;
return deficit > 0 ? `${base} deficit ${deficit}` : base;
}
function meshConnectionIntentSummary(heartbeat?: NodeHeartbeat) {
const report = heartbeat?.metadata?.mesh_peer_connection_intent_report;
if (!report || typeof report !== "object" || Array.isArray(report)) {
return meshConnectionManagerSummary(heartbeat);
}
const fields = report as Record<string, unknown>;
const intentCount = typeof fields.intent_count === "number" ? fields.intent_count : 0;
const maintain = typeof fields.maintain_count === "number" ? fields.maintain_count : 0;
const recover = typeof fields.recover_count === "number" ? fields.recover_count : 0;
const rendezvous = typeof fields.rendezvous_required_count === "number" ? fields.rendezvous_required_count : 0;
const resolved = typeof fields.rendezvous_resolved_count === "number" ? fields.rendezvous_resolved_count : 0;
const relayControl = typeof fields.relay_control_count === "number" ? fields.relay_control_count : 0;
const rvPart = [`rv${rendezvous}`];
if (resolved > 0) {
rvPart.push(`ok${resolved}`);
}
if (relayControl > 0) {
rvPart.push(`relay${relayControl}`);
}
const base =
rendezvous > 0 || resolved > 0 || relayControl > 0
? `${intentCount} intents m${maintain}/r${recover} ${rvPart.join("/")}`
: `${intentCount} intents m${maintain}/r${recover}`;
const manager = meshConnectionManagerSummary(heartbeat);
return manager === "н/д" ? base : `${base}; ${manager}`;
}
function meshRendezvousLeaseSummary(heartbeat?: NodeHeartbeat) {
const report = heartbeat?.metadata?.mesh_rendezvous_lease_report;
if (!report || typeof report !== "object" || Array.isArray(report)) {
return "н/д";
}
const fields = report as Record<string, unknown>;
const total = typeof fields.lease_count === "number" ? fields.lease_count : 0;
const active = typeof fields.active_count === "number" ? fields.active_count : 0;
const relay = typeof fields.admitted_as_relay_count === "number" ? fields.admitted_as_relay_count : 0;
const peer = typeof fields.admitted_as_peer_count === "number" ? fields.admitted_as_peer_count : 0;
const renewal = typeof fields.renewal_needed_count === "number" ? fields.renewal_needed_count : 0;
const ready = typeof fields.relay_control_ready_count === "number" ? fields.relay_control_ready_count : 0;
const stale = typeof fields.stale_relay_count === "number" ? fields.stale_relay_count : 0;
const refreshAttempts = typeof fields.refresh_attempt_count === "number" ? fields.refresh_attempt_count : 0;
const refreshSuccess = typeof fields.refresh_success_count === "number" ? fields.refresh_success_count : 0;
const parts = [`lease ${active}/${total}`];
if (relay > 0) {
parts.push(`relay${relay}`);
}
if (peer > 0) {
parts.push(`peer${peer}`);
}
if (renewal > 0) {
parts.push(`renew${renewal}`);
}
if (stale > 0) {
parts.push(`stale${stale}`);
}
if (ready > 0) {
parts.push(`ready${ready}`);
}
if (refreshAttempts > 0) {
parts.push(`ref${refreshSuccess}/${refreshAttempts}`);
}
return parts.join(" ");
}
function meshRoutePathDecisionSummary(heartbeat?: NodeHeartbeat) {
const report = heartbeat?.metadata?.mesh_route_path_decision_report;
if (!report || typeof report !== "object" || Array.isArray(report)) {
return "н/д";
}
const fields = report as Record<string, unknown>;
const total = typeof fields.decision_count === "number" ? fields.decision_count : 0;
const replacements = typeof fields.replacement_decision_count === "number" ? fields.replacement_decision_count : 0;
const degraded = typeof fields.degraded_decision_count === "number" ? fields.degraded_decision_count : 0;
const recoveryHysteresis = typeof fields.recovery_hysteresis_count === "number" ? fields.recovery_hysteresis_count : 0;
const recoveryPromoted = typeof fields.recovery_promoted_count === "number" ? fields.recovery_promoted_count : 0;
const recoveryDemoted = typeof fields.recovery_demoted_count === "number" ? fields.recovery_demoted_count : 0;
const local = typeof fields.local_effective_path_count === "number" ? fields.local_effective_path_count : 0;
const next = typeof fields.next_hop_available_count === "number" ? fields.next_hop_available_count : 0;
const withdrawn = typeof fields.withdrawn_local_relay_count === "number" ? fields.withdrawn_local_relay_count : 0;
const parts = [`path ${local}/${total}`];
if (replacements > 0) {
parts.push(`repl${replacements}`);
}
if (degraded > 0) {
parts.push(`degr${degraded}`);
}
if (recoveryHysteresis > 0) {
parts.push(`rec${recoveryHysteresis}`);
}
if (recoveryPromoted > 0) {
parts.push(`prom${recoveryPromoted}`);
}
if (recoveryDemoted > 0) {
parts.push(`dem${recoveryDemoted}`);
}
if (next > 0) {
parts.push(`next${next}`);
}
if (withdrawn > 0) {
parts.push(`wd${withdrawn}`);
}
return parts.join(" ");
}
function meshRouteGenerationSummary(heartbeat?: NodeHeartbeat) {
const report = heartbeat?.metadata?.mesh_route_generation_report;
if (!report || typeof report !== "object" || Array.isArray(report)) {
return "н/д";
}
const fields = report as Record<string, unknown>;
const active = typeof fields.active_decision_count === "number" ? fields.active_decision_count : 0;
const applied = typeof fields.applied_decision_count === "number" ? fields.applied_decision_count : 0;
const withdrawn = typeof fields.withdrawn_decision_count === "number" ? fields.withdrawn_decision_count : 0;
const changed = fields.generation_changed === true;
const parts = [`gen ${active}`];
if (applied > 0) {
parts.push(`ap${applied}`);
}
if (withdrawn > 0) {
parts.push(`wd${withdrawn}`);
}
if (changed) {
parts.push("chg");
}
return parts.join(" ");
}
function meshRouteHealthConfigSummary(heartbeat?: NodeHeartbeat) {
const report = heartbeat?.metadata?.mesh_route_health_config_report;
if (!report || typeof report !== "object" || Array.isArray(report)) {
return "н/д";
}
const fields = report as Record<string, unknown>;
const feedbackReport = heartbeat?.metadata?.mesh_route_health_feedback_refresh_report;
const feedbackFields =
feedbackReport && typeof feedbackReport === "object" && !Array.isArray(feedbackReport)
? (feedbackReport as Record<string, unknown>)
: {};
const routes = typeof fields.route_health_route_count === "number" ? fields.route_health_route_count : 0;
const applied = typeof fields.route_path_decision_applied_count === "number" ? fields.route_path_decision_applied_count : 0;
const replacements = typeof fields.replacement_route_health_route_count === "number" ? fields.replacement_route_health_route_count : 0;
const drift = typeof fields.route_health_decision_drift_candidate_count === "number" ? fields.route_health_decision_drift_candidate_count : 0;
const feedbackAttempts =
typeof feedbackFields.feedback_refresh_attempt_count === "number"
? feedbackFields.feedback_refresh_attempt_count
: typeof fields.feedback_refresh_attempt_count === "number"
? fields.feedback_refresh_attempt_count
: 0;
const feedbackSuccesses =
typeof feedbackFields.feedback_refresh_success_count === "number"
? feedbackFields.feedback_refresh_success_count
: typeof fields.feedback_refresh_success_count === "number"
? fields.feedback_refresh_success_count
: 0;
const feedbackSuppressed =
typeof feedbackFields.feedback_refresh_suppressed_count === "number"
? feedbackFields.feedback_refresh_suppressed_count
: typeof fields.feedback_refresh_suppressed_count === "number"
? fields.feedback_refresh_suppressed_count
: 0;
const parts = [`rh ${applied}/${routes}`];
if (replacements > 0) {
parts.push(`repl${replacements}`);
}
if (drift > 0) {
parts.push(`drift${drift}`);
}
if (feedbackAttempts > 0 || feedbackSuppressed > 0) {
parts.push(`fb${feedbackSuccesses}/${feedbackAttempts}`);
}
if (feedbackSuppressed > 0) {
parts.push(`sup${feedbackSuppressed}`);
}
return parts.join(" ");
}
function meshConnectionManagerSummary(heartbeat?: NodeHeartbeat) {
const report = heartbeat?.metadata?.mesh_peer_connection_manager_report;
if (!report || typeof report !== "object" || Array.isArray(report)) {
return "н/д";
}
const fields = report as Record<string, unknown>;
if (fields.enabled === false) {
return "manager off";
}
const attempted = typeof fields.attempted === "number" ? fields.attempted : 0;
const succeeded = typeof fields.succeeded === "number" ? fields.succeeded : 0;
const deferred = typeof fields.deferred === "number" ? fields.deferred : 0;
const relayControl = typeof fields.relay_control_count === "number" ? fields.relay_control_count : 0;
const base = relayControl > 0 ? `mgr ${succeeded}/${attempted} relay${relayControl}` : `mgr ${succeeded}/${attempted}`;
return deferred > 0 ? `${base} def${deferred}` : base;
}
function meshListenerSummary(heartbeat?: NodeHeartbeat) {
const report = heartbeat?.metadata?.mesh_listener_report;
if (!report || typeof report !== "object" || Array.isArray(report)) {
return "н/д";
}
const fields = report as Record<string, unknown>;
const status = typeof fields.status === "string" ? fields.status : "unknown";
const mode = typeof fields.listen_port_mode === "string" ? fields.listen_port_mode : "manual";
const effective = typeof fields.effective_listen_addr === "string" ? fields.effective_listen_addr : "";
const reason = typeof fields.failure_reason === "string" ? fields.failure_reason : "";
if (status === "listening") {
return effective ? `listen ${effective}` : "listen";
}
if (status === "auto_rebound") {
return effective ? `auto ${effective}` : "auto rebound";
}
if (status === "listen_failed") {
return reason ? `${mode} failed: ${reason}` : `${mode} failed`;
}
if (status === "disabled") {
return mode === "disabled" ? "inbound off" : "inbound unavailable";
}
return status;
}
function clusterCountLabel(count: number, language: Language) {
if (language === "en") {
return count === 1 ? "1 cluster" : `${count} clusters`;
}
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) {
return `${count} кластер`;
}
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) {
return `${count} кластера`;
}
return `${count} кластеров`;
}
function linkTone(link: MeshLink, status: TopologyLinkStatus = link.link_status === "reachable" ? "reachable" : "unknown") {
if (status === "stale") {
return "stale";
}
if (status === "one_way") {
return "oneWay";
}
if (status !== "reachable" || link.link_status !== "reachable") {
return "bad";
}
if (link.quality_score != null && link.quality_score < 70) {
return "weak";
}
if (link.latency_ms != null && link.latency_ms > 80) {
return "weak";
}
return "good";
}
function trimNodeName(value: string, maxLength = 16) {
return value.length > maxLength ? `${value.slice(0, Math.max(1, maxLength - 2))}` : value;
}
function confirmHighRisk(action: string) {
return window.confirm(`${action}?\n\nЭто высокорисковая операция владельца платформы. Действие будет записано в аудит.`);
}
function downloadBaseUrl(apiBaseUrl: string) {
const normalized = (apiBaseUrl || "").replace(/\/$/, "");
if (!normalized || normalized === "/api/v1") {
return window.location.origin;
}
if (normalized.endsWith("/api/v1")) {
return normalized.slice(0, -"/api/v1".length);
}
return normalized;
}
function shortId(value?: string | null) {
if (!value) {
return "нет";
}
return value.length > 12 ? `${value.slice(0, 8)}...${value.slice(-4)}` : value;
}
function formatDate(value?: string | null) {
if (!value) {
return "никогда";
}
return new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(value));
}
function formatAgeSeconds(value?: number | null) {
if (value == null || Number.isNaN(value)) {
return "age n/a";
}
if (value < 60) {
return `${Math.max(0, Math.round(value))}s ago`;
}
if (value < 3600) {
return `${Math.round(value / 60)}m ago`;
}
if (value < 86400) {
return `${Math.round(value / 3600)}h ago`;
}
return `${Math.round(value / 86400)}d ago`;
}
function formatTime(value?: string | null) {
if (!value) {
return "н/д";
}
return new Intl.DateTimeFormat(undefined, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(new Date(value));
}
function formatBytes(value?: number | null) {
if (value == null || Number.isNaN(value)) {
return "н/д";
}
const units = ["B", "KB", "MB", "GB", "TB"];
let size = value;
let index = 0;
while (size >= 1024 && index < units.length - 1) {
size /= 1024;
index += 1;
}
return `${size.toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
}
function formatTrafficClassCounts(value?: Record<string, number> | null) {
if (!value || Object.keys(value).length === 0) {
return "qos none";
}
return ["control", "interactive", "reliable", "bulk", "droppable"]
.filter((key) => (value[key] || 0) > 0)
.map((key) => `${key[0]}:${value[key]}`)
.join(" ") || "qos none";
}
function formatRecommendedWindows(value?: Record<string, number> | null) {
if (!value || Object.keys(value).length === 0) {
return "n/a";
}
return ["control", "interactive", "reliable", "bulk", "droppable"]
.filter((key) => (value[key] || 0) > 0)
.map((key) => `${key[0]}:${value[key]}`)
.join(" ") || "n/a";
}
function flowHealthPillClass(status?: string | null, dropped?: number | null) {
if ((dropped || 0) > 0 || status === "critical") {
return "bad";
}
if (status === "degraded") {
return "warn";
}
if (status === "watch") {
return "info";
}
return "good";
}
function remediationExecutionTone(status?: string) {
switch (status) {
case "applied":
case "rebuild_request_applied":
return "good";
case "waiting_node_apply":
case "pending_rebuild_request":
case "pending_degraded_fallback":
case "rebuild_request_recorded":
case "rebuild_request_recorded_node_pending":
case "rebuild_request_no_alternate":
case "rebuild_request_deferred_by_policy":
case "route_rebuild_no_safe_recovery":
return "warn";
case "expired":
case "rejected_by_policy_guard":
case "rebuild_request_rejected":
case "rebuild_request_expired":
return "bad";
default:
return status ? "bad" : "";
}
}
function routeDecisionTone(source?: string, status?: string, reasons?: string[]) {
if (source === "service_channel_feedback_no_alternate" || status === "pending_degraded_fallback" || (reasons || []).includes("no_unfenced_alternate_route")) {
return "warn";
}
if (status === "applied" || (reasons || []).includes("service_channel_rebuild_applied")) {
return "good";
}
if (source?.includes("replacement")) {
return "info";
}
return source || status ? "info" : "";
}
function statusLabel(value: string) {
const labels: Record<string, string> = {
active: "активно",
approved: "одобрено",
authoritative: "authoritative",
connecting: "подключается",
connected: "связан",
critical: "критично",
current: "актуальна",
degraded: "degraded",
disabled: "выключено",
enabled: "включено",
failed: "ошибка",
healthy: "здоров",
watch: "наблюдение",
flow_health_ready: "flow ready",
flow_drops_reported: "flow drops",
route_quality_window_drops_reported: "route drops",
backend_fallback_observed: "backend fallback",
route_quality_window_failures_reported: "route failures",
route_quality_window_slow_samples_reported: "slow samples",
route_send_latency_high: "high latency",
flow_queue_pressure_high: "queue pressure high",
bulk_pressure_with_interactive_qos_observed: "bulk+interactive",
bulk_pressure_observed: "bulk pressure",
flow_queue_pressure_observed: "queue pressure",
flow_health_degraded: "flow degraded",
bulk_window_reduced_to_protect_interactive: "bulk reduced",
rebuild_request_applied: "planner applied",
rebuild_request_recorded: "rebuild recorded",
rebuild_request_recorded_node_pending: "node pending",
rebuild_request_no_alternate: "no alternate",
rebuild_request_deferred_by_policy: "deferred by policy",
rebuild_request_rejected: "rebuild rejected",
rebuild_request_expired: "rebuild expired",
route_rebuild_no_safe_recovery: "no safe recovery",
access_decision: "access decision",
access_no_safe_recovery: "access no-safe",
access_recovery_selected: "access recovery",
access_rebuild_applied: "access applied",
access_replacement_selected: "access replacement",
inspect_access_no_safe_recovery_route_pool_and_signed_policy: "inspect no-safe route pool",
watch_recovery_route_quality_and_confirm_post_recovery_traffic: "watch recovery traffic",
confirm_applied_rebuild_runtime_traffic_stays_on_replacement: "confirm applied traffic",
watch_replacement_route_quality_until_applied_or_recovered: "watch replacement",
pending_degraded_fallback: "pending fallback",
service_channel_feedback_no_alternate: "no safe route",
service_channel_feedback_replacement: "replacement",
service_channel_feedback_exit_pool_replacement: "exit replacement",
service_channel_feedback_entry_pool_replacement: "entry replacement",
service_channel_feedback_entry_exit_pool_replacement: "pool replacement",
service_channel_remediation_command: "remediation",
service_channel_feedback_rebuild_requested: "rebuild requested",
remediation_rebuild_applied_to_alternate: "planner selected alternate",
no_unfenced_alternate_route: "no safe alternate",
active_lease_not_found_for_rebuild_resolution: "lease missing",
remediation_command_ttl_expired: "command expired",
durable_rebuild_route_request_recorded: "rebuild recorded",
durable_rebuild_route_request_rejected: "request rejected",
durable_rebuild_route_request_applied: "request applied",
durable_rebuild_route_no_alternate: "no alternate",
durable_rebuild_route_deferred_by_policy: "deferred by policy",
durable_rebuild_route_expired: "request expired",
isolated: "изолирован",
offline: "нет связи",
one_way: "односторонняя",
outdated: "обновить",
pending: "ожидает",
platform_managed: "платформенный",
promoted: "promoted",
rejected: "отклонено",
ready: "готово",
revoked: "отозвано",
running: "работает",
customer_managed: "клиентский",
no_policy: "нет политики",
not_configured: "не задано",
missing: "нет отчета",
service_channel_recovery_demoted: "demoted",
service_channel_recovery_demoted_degraded: "degraded",
service_channel_recovery_demoted_degraded_fallback: "fallback",
service_channel_recovery_demoted_failure: "failure",
service_channel_recovery_demoted_fenced: "fenced",
service_channel_recovery_demoted_rebuild: "rebuild",
service_channel_recovery_demoted_slow: "slow",
service_channel_feedback_provenance_missing: "provenance missing",
service_channel_feedback_stale: "stale feedback",
service_channel_feedback_stale_generation: "stale generation",
service_channel_feedback_stale_policy: "stale policy",
service_channel_feedback_stale_policy_and_generation: "stale policy+generation",
schema_ready: "schema ready",
schema_migration_required: "schema migration required",
snapshots_warmed: "snapshots warmed",
missing_snapshots_warmed_stale_deferred: "missing warmed, stale deferred",
snapshot_warmup_partial: "warmup partial",
stopped: "остановлено",
stale: "устарело",
unknown: "неизвестно",
};
return labels[value] || value;
}