рабочий вариант, но скороть 10 МБит
build / backend (push) Has been cancelled
build / node-agent (push) Has been cancelled
build / worker (push) Has been cancelled

This commit is contained in:
2026-05-22 21:46:49 +03:00
parent 469fa0e860
commit 20d361a886
280 changed files with 954890 additions and 18524 deletions
+17 -8
View File
@@ -9,7 +9,7 @@ organization users.
Architecture boundary:
- WEB is HTTP/HTTPS ingress and presentation.
- Cluster configuration belongs to Control Plane APIs.
- Cluster configuration is reached through the panel service gateway; the farm transport behind it remains QUIC fabric.
- PostgreSQL remains the source of truth.
- Dynamic admin pages must be safe, scoped, schema-driven projections.
- Secrets, internal topology, peer caches, route caches, and raw credentials
@@ -33,6 +33,7 @@ Implemented platform-owner sections:
- node inventory
- node membership disable and identity revoke boundaries
- join token creation with signed authority envelope visibility
- signed join bundle generation for docker/linux/windows node installs
- join request approve/reject with signed approval envelope visibility
- role assignment
- workload desired-state setting
@@ -86,18 +87,18 @@ http://127.0.0.1:5173
The admin console should run on a dedicated admin host/port. It is not intended
to be the public product landing page on generic `80/443` web ingress.
Default backend API inside the panel:
Panel service gateway inside the UI:
```text
/api/v1
```
The local Vite dev server proxies `/api` to the remote test backend
`http://192.168.200.61:8080` by default, avoiding browser CORS issues while
keeping the Control Plane API unchanged. Override the proxy target when needed:
The browser never asks the operator for a farm HTTP endpoint. During local
development the Vite dev server proxies `/api` to the panel service gateway.
Override the local proxy target only for developer work:
```powershell
$env:RAP_ADMIN_API_PROXY = "http://192.168.200.61:8080"
$env:RAP_ADMIN_API_PROXY = "http://<panel-service-host>:<port>"
npm run dev -- --port 5173
```
@@ -108,7 +109,7 @@ password fields; it does not expose API URLs or language/settings to
unauthenticated users.
After authentication the panel verifies platform-owner/platform-admin access
through Control Plane APIs before opening the console. Users without product-owner
through the panel service gateway before opening the console. Users without product-owner
scope must not see this panel. Organization admins and organization users require
separate scoped panels.
@@ -116,10 +117,18 @@ Language selection is available only after login in the profile area. It is stor
as a user-scoped browser preference for this MVP. Backend user-profile persistence
for language/locale is a later Control Plane profile setting.
The panel shows real Control Plane data only. If cluster counts are zero, the
The panel shows real farm data only. If cluster counts are zero, the
cluster has no approved node-agent nodes, roles, workloads, VPN records, or mesh
observations yet.
Current node enrollment surface is bundle-first:
- the panel creates a one-time install token
- the panel requests a signed join bundle from the panel service gateway
- the operator downloads or posts that bundle to the target machine
- `rap-host-agent install* --join-bundle ...` performs first install
- after first start, node enrollment, update, and control traffic use QUIC fabric
## Safety Rules
- The console is platform-owner/platform-admin only.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+90 -4
View File
@@ -3,11 +3,97 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Панель Secure Access Fabric</title>
<script type="module" crossorigin src="/assets/index-CiNvRobk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cur_BAkX.css">
<title>Вход в панель фермы</title>
<style>
:root {
color-scheme: light;
--bg: #eef2ec;
--panel: #fffdf7;
--ink: #102018;
--muted: #66716a;
--line: #dce2d8;
--accent: #1f6b4c;
--soft: #f6f4ea;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: Inter, "Segoe UI", Arial, sans-serif;
font-size: 15px;
line-height: 1.45;
}
main {
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px;
}
.shell { width: min(460px, 100%); }
h1 { margin: 0 0 8px; font-size: 26px; letter-spacing: 0; }
p { margin: 0; color: var(--muted); }
form, .note {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 18px;
}
form { margin-top: 22px; box-shadow: 0 14px 34px rgba(20, 35, 25, .08); }
label {
display: block;
margin: 0 0 14px;
color: #3d4a42;
font-size: 13px;
font-weight: 800;
}
input {
width: 100%;
margin-top: 7px;
border: 1px solid var(--line);
border-radius: 6px;
background: white;
color: var(--ink);
padding: 12px 13px;
font: inherit;
}
input:focus { outline: 2px solid rgba(31, 107, 76, .22); border-color: var(--accent); }
button {
width: 100%;
border: 0;
border-radius: 6px;
background: var(--accent);
color: white;
padding: 12px 14px;
font: 800 15px Inter, "Segoe UI", Arial, sans-serif;
cursor: pointer;
}
.note {
margin-top: 10px;
background: var(--soft);
color: var(--muted);
font-size: 13px;
}
</style>
</head>
<body>
<div id="root"></div>
<main>
<section class="shell" aria-labelledby="title">
<h1 id="title">Вход в панель фермы</h1>
<p>Администратор откроет управление фермой. Пользователь получит страницу установки VPN.</p>
<form method="post" action="/api/v1/auth/ui/login">
<label>
Email или логин
<input name="email" autocomplete="username" required autofocus />
</label>
<label>
Пароль
<input name="password" type="password" autocomplete="current-password" required />
</label>
<button type="submit">Войти</button>
</form>
<p class="note">Вход выполняется сервером. В браузере нет хранения ID оператора и тяжелой клиентской логики.</p>
</section>
</main>
</body>
</html>
+82 -3
View File
@@ -3,10 +3,89 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Панель Secure Access Fabric</title>
<title>Панель управления фермой</title>
<style>
:root { color: #172019; background: #eef0ea; font-family: system-ui, "Segoe UI", sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; }
main { max-width: 1180px; margin: 0 auto; padding: 20px; }
header { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); gap: 14px; align-items: stretch; margin-bottom: 12px; }
h1 { margin: 0; font-size: 1.65rem; letter-spacing: 0; }
h2 { margin: 0 0 8px; font-size: 1rem; }
p { margin: 0; color: #667064; line-height: 1.45; }
.hero, .identity, .panel { border: 1px solid #1a271b26; border-radius: 8px; background: #fffdf5; padding: 14px; }
.hero { display: grid; align-content: space-between; min-height: 180px; }
.badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.pill { display: inline-flex; width: fit-content; border-radius: 8px; padding: .24rem .5rem; font-size: .74rem; font-weight: 850; background: #36556c1a; color: #36556c; }
.good { color: #236c4a; background: #2f6f4f1f; }
.warn { color: #9a5b1c; background: #b86f2324; }
form { display: grid; gap: 10px; }
label { display: grid; gap: 6px; color: #667064; font-size: .78rem; font-weight: 850; }
input, select, button { min-height: 38px; border: 1px solid #1a271b26; border-radius: 8px; padding: .5rem .65rem; color: #172019; background: #fff; font: inherit; }
button { border-color: #2f6f4f; background: #2f6f4f; color: #fffaf0; font-weight: 850; cursor: pointer; }
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.panel { min-height: 120px; }
.panel strong { display: block; margin-bottom: 4px; }
.panel span { color: #667064; font-size: .84rem; }
.footer { margin-top: 12px; color: #667064; font-size: .82rem; }
@media (max-width: 860px) { main { padding: 10px; } header, .grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<main>
<header>
<section class="hero">
<div>
<h1>Панель управления фермой</h1>
<p>Рабочая консоль для идентификации оператора, обзора узлов, обновлений, QUIC fabric, веб-контроля и аудита.</p>
</div>
<div class="badges">
<span class="pill good">HTML5</span>
<span class="pill good">HTMX-lite</span>
<span class="pill">серверные расчеты</span>
<span class="pill warn">внутри фермы QUIC fabric</span>
</div>
</section>
<section class="identity">
<h2>Идентификация оператора</h2>
<form id="admin-form" action="/api/v1/ui/admin" method="get">
<label>
ID пользователя
<input id="actor-user-id" name="actor_user_id" autocomplete="username" required placeholder="uuid оператора" />
</label>
<label>
Область входа
<select name="scope">
<option value="farm">ферма</option>
<option value="cluster">кластер</option>
<option value="organization">организация</option>
</select>
</label>
<button type="submit">Открыть консоль</button>
</form>
</section>
</header>
<section class="grid">
<div class="panel"><strong>Узлы</strong><span>живость, версии, heartbeat, роли, детали обновлений.</span></div>
<div class="panel"><strong>Fabric</strong><span>визуальная работа связей, service channels, readiness и политики восстановления.</span></div>
<div class="panel"><strong>Веб-контроль</strong><span>настройка admin-ingress и public-ingress как workloads фермы.</span></div>
<div class="panel"><strong>Обновления</strong><span>массовые signed update hints, релизы, статусы и rollback-контуры.</span></div>
<div class="panel"><strong>Аналитика</strong><span>маршруты, rebuild health, access telemetry, активные каналы.</span></div>
<div class="panel"><strong>Аудит</strong><span>операторские действия, изменения политик, события восстановления.</span></div>
</section>
<div class="footer">ID сохраняется только в локальном профиле браузера для быстрого повторного входа.</div>
</main>
<script>
(() => {
const input = document.getElementById("actor-user-id");
const form = document.getElementById("admin-form");
if (!input || !form) return;
const stored = localStorage.getItem("rap.webAdmin.actorUserId");
if (stored) input.value = stored;
form.addEventListener("submit", () => {
if (input.value.trim()) localStorage.setItem("rap.webAdmin.actorUserId", input.value.trim());
});
})();
</script>
</body>
</html>
+807 -587
View File
File diff suppressed because it is too large Load Diff
+71 -12
View File
@@ -9,6 +9,7 @@ import type {
ClusterAuthorityState,
ClusterNode,
CreatedJoinToken,
InstallJoinBundle,
FabricServiceChannelAdaptivePolicy,
FabricServiceChannelBreadcrumbWindowPolicy,
FabricServiceChannelPoolPolicy,
@@ -58,10 +59,11 @@ import type {
} from "../types";
export type AdminClientConfig = {
baseUrl: string;
actorUserId: string;
};
const panelGatewayBasePath = "/api/v1";
type ApiErrorPayload = {
error?: {
code?: string;
@@ -155,7 +157,7 @@ export type UpdateFabricServiceChannelPoolPolicyPayload = {
routeRebuild?: string;
entryFailover?: string;
exitFailover?: string;
backendFallbackAllowed?: boolean;
degradedRouteAllowed?: boolean;
stickySession?: boolean;
};
@@ -196,11 +198,9 @@ export type UpsertResourceSecretPayload = {
};
export class AdminApiClient {
private readonly baseUrl: string;
private readonly actorUserId: string;
constructor(config: AdminClientConfig) {
this.baseUrl = config.baseUrl.replace(/\/$/, "");
this.actorUserId = config.actorUserId.trim();
}
@@ -377,6 +377,51 @@ export class AdminApiClient {
return payload.join_token;
}
async getDockerJoinBundle(input: {
clusterId: string;
installToken: string;
nodeName?: string;
hostFacts?: Record<string, unknown>;
}): Promise<InstallJoinBundle> {
const payload = await this.post<{ join_bundle: InstallJoinBundle }>(`/node-agents/docker-join-bundle`, {
cluster_id: input.clusterId,
install_token: input.installToken,
node_name: input.nodeName || "",
host_facts: input.hostFacts || {},
});
return payload.join_bundle;
}
async getWindowsJoinBundle(input: {
clusterId: string;
installToken: string;
nodeName?: string;
hostFacts?: Record<string, unknown>;
}): Promise<InstallJoinBundle> {
const payload = await this.post<{ join_bundle: InstallJoinBundle }>(`/node-agents/windows-join-bundle`, {
cluster_id: input.clusterId,
install_token: input.installToken,
node_name: input.nodeName || "",
host_facts: input.hostFacts || {},
});
return payload.join_bundle;
}
async getLinuxJoinBundle(input: {
clusterId: string;
installToken: string;
nodeName?: string;
hostFacts?: Record<string, unknown>;
}): Promise<InstallJoinBundle> {
const payload = await this.post<{ join_bundle: InstallJoinBundle }>(`/node-agents/linux-join-bundle`, {
cluster_id: input.clusterId,
install_token: input.installToken,
node_name: input.nodeName || "",
host_facts: input.hostFacts || {},
});
return payload.join_bundle;
}
async listJoinTokens(clusterId: string): Promise<NodeJoinToken[]> {
const payload = await this.get<{ join_tokens: NodeJoinToken[] }>(`/clusters/${clusterId}/join-tokens`);
return payload.join_tokens ?? [];
@@ -1021,7 +1066,7 @@ export class AdminApiClient {
route_rebuild: input.routeRebuild,
entry_failover: input.entryFailover,
exit_failover: input.exitFailover,
backend_fallback_allowed: input.backendFallbackAllowed,
degraded_route_allowed: input.degradedRouteAllowed,
sticky_session: input.stickySession,
},
);
@@ -1193,7 +1238,7 @@ export class AdminApiClient {
}
clusterEventsURL(clusterId: string): string {
return `${this.baseUrl}/clusters/${encodeURIComponent(clusterId)}/events?actor_user_id=${encodeURIComponent(this.actorUserId)}`;
return `${panelGatewayBasePath}/clusters/${encodeURIComponent(clusterId)}/events?actor_user_id=${encodeURIComponent(this.actorUserId)}`;
}
async getOrganizationAdminSummary(organizationId: string): Promise<OrganizationAdminSummary> {
@@ -1325,14 +1370,20 @@ export class AdminApiClient {
}
private async request<T>(path: string, init: RequestInit): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, init);
const response = await fetch(`${panelGatewayBasePath}${path}`, {
...init,
headers: {
"X-RAP-Panel-Gateway": "fabric",
...(init.headers || {}),
},
});
if (!response.ok) {
let message = `Запрос завершился ошибкой HTTP ${response.status}`;
let message = `Запрос завершился ошибкой ${response.status}`;
try {
const payload = (await response.json()) as ApiErrorPayload;
message = formatApiErrorMessage(payload, response.status) || payload.error?.fallback_message || payload.error?.code || message;
} catch {
// Keep generic HTTP message if backend did not return JSON.
// Keep generic message if backend did not return JSON.
}
throw new Error(message);
}
@@ -1345,9 +1396,9 @@ function formatApiErrorMessage(payload: ApiErrorPayload, status: number) {
if (!error) {
return "";
}
if (status === 409 && error.code === "conflict.legacy_compatibility_removal_is_blocked_while_stale_recovery_risk_nodes_remain") {
if (status === 409 && error.code === "conflict.fabric_standard_cleanup_is_blocked_while_stale_recovery_risk_nodes_remain") {
const details = error.details || {};
const parts: string[] = ["Compatibility cleanup заблокирован."];
const parts: string[] = ["Fabric standard cleanup заблокирован."];
const blockedOperation = stringDetail(details, "blocked_operation");
if (blockedOperation) {
parts.push(`Операция: ${blockedOperation}.`);
@@ -1356,10 +1407,18 @@ function formatApiErrorMessage(payload: ApiErrorPayload, status: number) {
numberDetail(details, "blocked_nodes") ? `blockers ${numberDetail(details, "blocked_nodes")}` : "",
numberDetail(details, "stale_nodes") ? `stale ${numberDetail(details, "stale_nodes")}` : "",
numberDetail(details, "artifact_gap_nodes") ? `artifact gap ${numberDetail(details, "artifact_gap_nodes")}` : "",
numberDetail(details, "area_diversity_alert_nodes") ? `area diversity ${numberDetail(details, "area_diversity_alert_nodes")}` : "",
numberDetail(details, "independent_ingress_alert_nodes") ? `independent ingress ${numberDetail(details, "independent_ingress_alert_nodes")}` : "",
numberDetail(details, "updater_wake_unsupported_nodes") ? `updater wake unsupported ${numberDetail(details, "updater_wake_unsupported_nodes")}` : "",
numberDetail(details, "updater_runtime_missing_nodes") ? `updater runtime missing ${numberDetail(details, "updater_runtime_missing_nodes")}` : "",
numberDetail(details, "standard_updater_line_nodes") ? `standard updater line ${numberDetail(details, "standard_updater_line_nodes")}` : "",
numberDetail(details, "staged_self_update_pending_nodes") ? `staged self-update ${numberDetail(details, "staged_self_update_pending_nodes")}` : "",
numberDetail(details, "standard_control_dependency_nodes") ? `standard control ${numberDetail(details, "standard_control_dependency_nodes")}` : "",
numberDetail(details, "registry_candidate_only_nodes") ? `registry candidate-only ${numberDetail(details, "registry_candidate_only_nodes")}` : "",
numberDetail(details, "unknown_profile_nodes") ? `profile unknown ${numberDetail(details, "unknown_profile_nodes")}` : "",
numberDetail(details, "waiting_update_status_nodes") ? `waiting status ${numberDetail(details, "waiting_update_status_nodes")}` : "",
numberDetail(details, "unknown_version_nodes") ? `version unknown ${numberDetail(details, "unknown_version_nodes")}` : "",
numberDetail(details, "legacy_recovery_contract_nodes") ? `legacy contract ${numberDetail(details, "legacy_recovery_contract_nodes")}` : "",
numberDetail(details, "standard_recovery_contract_nodes") ? `standard contract ${numberDetail(details, "standard_recovery_contract_nodes")}` : "",
numberDetail(details, "recovery_bridge_required_nodes") ? `recovery bridge ${numberDetail(details, "recovery_bridge_required_nodes")}` : "",
numberDetail(details, "recovery_bridge_replay_ready_nodes") ? `bridge replay ready ${numberDetail(details, "recovery_bridge_replay_ready_nodes")}` : "",
numberDetail(details, "waiting_recovery_heartbeat_nodes") ? `waiting heartbeat ${numberDetail(details, "waiting_recovery_heartbeat_nodes")}` : "",
+314 -210
View File
File diff suppressed because it is too large Load Diff
+119 -16
View File
@@ -150,6 +150,74 @@ export type CreatedJoinToken = {
export type NodeJoinToken = Omit<CreatedJoinToken, "token">;
export type DockerArtifact = {
kind: string;
image?: string;
media_type?: string;
file_name?: string;
urls?: string[];
sha256?: string;
size_bytes?: number;
};
export type BootstrapInstallProfile = {
schema_version: string;
cluster_id: string;
cluster_authority_public_key?: string;
artifact_endpoints?: string[];
fabric_registry_records?: unknown[];
join_token: string;
node_name: string;
state_dir: string;
mesh_advertise_endpoint?: string;
mesh_advertise_endpoints_json?: unknown;
mesh_advertise_transport?: string;
mesh_connectivity_mode?: string;
mesh_nat_type?: string;
mesh_region?: string;
fabric_listen_addr?: string;
fabric_listen_port_mode?: string;
fabric_listen_auto_port_start?: number;
fabric_listen_auto_port_end?: number;
roles?: string[];
};
export type DockerJoinInstallProfile = BootstrapInstallProfile & {
image: string;
container_name: string;
network: string;
restart_policy: string;
pull_image: boolean;
replace: boolean;
docker_image_artifact?: DockerArtifact;
};
export type WindowsJoinInstallProfile = BootstrapInstallProfile & {
install_dir: string;
startup_mode: string;
node_agent_artifact?: DockerArtifact;
};
export type LinuxJoinInstallProfile = BootstrapInstallProfile & {
install_dir: string;
startup_mode: string;
replace?: boolean;
node_agent_artifact?: DockerArtifact;
};
export type InstallJoinBundle = {
schema_version: string;
bundle_kind: string;
cluster_id: string;
cluster_authority?: ClusterAuthorityDescriptor;
authority_payload?: Record<string, unknown>;
authority_signature?: ClusterSignature;
issued_at: string;
docker_install_profile?: DockerJoinInstallProfile;
windows_install_profile?: WindowsJoinInstallProfile;
linux_install_profile?: LinuxJoinInstallProfile;
};
export type RoleAssignment = {
id: string;
cluster_id: string;
@@ -412,6 +480,8 @@ export type StaleNodeRiskProduct = {
last_status_phase?: string | null;
last_status_value?: string | null;
last_status_reason?: string | null;
staged_self_update_pending?: boolean;
post_update_heartbeat_gap?: boolean;
recovery_bridge_required?: boolean;
recovery_bridge_replay_ready?: boolean;
recovery_bridge_mode?: string | null;
@@ -421,6 +491,7 @@ export type StaleNodeRiskProduct = {
export type StaleNodeRiskNode = {
node_id: string;
name: string;
area?: string | null;
node_key?: string;
reported_version?: string | null;
health_status: string;
@@ -432,6 +503,27 @@ export type StaleNodeRiskNode = {
direct_peer_ready_count?: number;
direct_peer_target_count?: number;
direct_peer_deficit?: number;
direct_ready_areas?: string[];
external_area_ready_count?: number;
required_external_area_count?: number;
area_diversity_alert?: boolean;
required_independent_ingress_count?: number;
independent_ingress_alert?: boolean;
full_directory_expected?: boolean;
known_peer_directory_count?: number;
expected_directory_count?: number;
directory_dissemination_alert?: boolean;
updater_subscription_alert?: boolean;
updater_wake_unsupported?: boolean;
updater_runtime_missing?: boolean;
standard_updater_line?: boolean;
staged_self_update_pending?: boolean;
post_update_heartbeat_gap?: boolean;
standard_control_dependency?: boolean;
standard_control_url?: string | null;
registry_runtime_status?: string | null;
resolved_service_count?: number;
independent_ingress_count?: number;
alerts?: string[];
recovery_bridge_required?: boolean;
recovery_bridge_replay_ready?: boolean;
@@ -445,11 +537,22 @@ export type StaleNodeRiskSummary = {
stale_nodes: number;
blocked_nodes: number;
direct_peer_alert_nodes?: number;
area_diversity_alert_nodes?: number;
independent_ingress_alert_nodes?: number;
directory_dissemination_alert_nodes?: number;
updater_subscription_alert_nodes?: number;
updater_wake_unsupported_nodes?: number;
updater_runtime_missing_nodes?: number;
standard_updater_line_nodes?: number;
staged_self_update_pending_nodes?: number;
post_update_heartbeat_gap_nodes?: number;
artifact_gap_nodes?: number;
standard_control_dependency_nodes?: number;
registry_candidate_only_nodes?: number;
unknown_profile_nodes?: number;
waiting_update_status_nodes?: number;
unknown_version_nodes?: number;
legacy_recovery_contract_nodes?: number;
standard_recovery_contract_nodes?: number;
recovery_bridge_required_nodes?: number;
recovery_bridge_replay_ready_nodes?: number;
waiting_recovery_heartbeat_nodes?: number;
@@ -459,7 +562,7 @@ export type StaleNodeRiskReport = {
cluster_id: string;
generated_at: string;
heartbeat_stale_after_seconds?: number;
legacy_removal_allowed: boolean;
fabric_standard_cleanup_allowed: boolean;
bridge_hold_required?: boolean;
bridge_hold_node_ids?: string[];
bridge_hold_reasons?: string[];
@@ -914,7 +1017,7 @@ export type FabricServiceChannelLeaseSummary = {
primary_route_id?: string;
primary_route_status?: string;
data_plane?: FabricServiceChannelDataPlaneContract;
force_backend_fallback: boolean;
force_degraded_route: boolean;
expired: boolean;
issued_at: string;
expires_at: string;
@@ -965,15 +1068,15 @@ export type FabricServiceChannelAccessTelemetryNode = {
total_accepted: number;
signed_accepted: number;
introspection_accepted: number;
legacy_unsigned_accepted: number;
backend_fallback_count: number;
backend_fallback_blocked_count?: number;
unsigned_accepted: number;
degraded_route_use_count: number;
degraded_route_blocked_count?: number;
fabric_route_send_failure_count?: number;
data_plane_contract_count?: number;
last_data_plane_mode?: string;
last_working_data_transport?: string;
last_steady_state_transport?: string;
last_backend_relay_policy?: string;
last_degraded_route_policy?: string;
last_logical_flow_mode?: string;
last_data_plane_violation_status?: string;
last_data_plane_violation_reason?: string;
@@ -1000,17 +1103,17 @@ export type FabricServiceChannelAccessTelemetryChannel = {
selected_exit_node_id?: string;
primary_route_id?: string;
primary_route_status?: string;
force_backend_fallback: boolean;
force_degraded_route: boolean;
entry_node_total_accepted: number;
entry_node_introspection_accepted: number;
entry_node_backend_fallback_count: number;
entry_node_backend_fallback_blocked_count?: number;
entry_node_degraded_route_count: number;
entry_node_degraded_route_blocked_count?: number;
entry_node_fabric_route_send_failure_count?: number;
entry_node_data_plane_contract_count?: number;
entry_node_last_data_plane_mode?: string;
entry_node_last_working_data_transport?: string;
entry_node_last_steady_state_transport?: string;
entry_node_last_backend_relay_policy?: string;
entry_node_last_degraded_route_policy?: string;
entry_node_last_logical_flow_mode?: string;
entry_node_last_data_plane_violation_status?: string;
entry_node_last_data_plane_violation_reason?: string;
@@ -1094,15 +1197,15 @@ export type FabricServiceChannelAccessTelemetry = {
total_accepted: number;
signed_accepted: number;
introspection_accepted: number;
legacy_unsigned_accepted: number;
backend_fallback_count: number;
backend_fallback_blocked_count?: number;
unsigned_accepted: number;
degraded_route_use_count: number;
degraded_route_blocked_count?: number;
fabric_route_send_failure_count?: number;
data_plane_contract_count?: number;
last_data_plane_mode?: string;
last_working_data_transport?: string;
last_steady_state_transport?: string;
last_backend_relay_policy?: string;
last_degraded_route_policy?: string;
last_logical_flow_mode?: string;
last_data_plane_violation_status?: string;
last_data_plane_violation_reason?: string;
@@ -1250,7 +1353,7 @@ export type FabricServiceChannelPoolPolicy = {
route_rebuild: string;
entry_failover: string;
exit_failover: string;
backend_fallback_allowed: boolean;
degraded_route_allowed: boolean;
sticky_session: boolean;
source: string;
updated_by_user_id?: string | null;