3
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
||||
<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-gMV--oab.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CiNvRobk.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cur_BAkX.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+827
-55
File diff suppressed because it is too large
Load Diff
+133
-1
@@ -36,6 +36,7 @@ import type {
|
||||
NodeSyntheticMeshConfig,
|
||||
NodeTelemetryObservation,
|
||||
NodeUpdatePlan,
|
||||
NodeBridgeReplayPlan,
|
||||
NodeUpdatePolicy,
|
||||
NodeUpdateStatus,
|
||||
NodeWorkloadDesiredState,
|
||||
@@ -46,6 +47,7 @@ import type {
|
||||
ReleaseVersion,
|
||||
Resource,
|
||||
RoleAssignment,
|
||||
StaleNodeRiskReport,
|
||||
UserAccount,
|
||||
VPNClientDiagnosticCommand,
|
||||
VPNClientDiagnosticStatus,
|
||||
@@ -66,6 +68,7 @@ type ApiErrorPayload = {
|
||||
message_key?: string;
|
||||
fallback_message?: string;
|
||||
trace_id?: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -106,6 +109,25 @@ export type UpsertNodeUpdatePolicyPayload = {
|
||||
healthWindowSeconds?: number;
|
||||
};
|
||||
|
||||
export type CreateReleaseVersionPayload = {
|
||||
product: string;
|
||||
version: string;
|
||||
channel?: string;
|
||||
status?: string;
|
||||
compatibility?: Record<string, unknown>;
|
||||
changelog?: string;
|
||||
artifacts: Array<{
|
||||
os: string;
|
||||
arch: string;
|
||||
installType: string;
|
||||
kind: string;
|
||||
url: string;
|
||||
sha256: string;
|
||||
sizeBytes?: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type UpdateFabricServiceChannelRecoveryPolicyPayload = {
|
||||
hysteresisPenalty?: number;
|
||||
promotionMinSamples?: number;
|
||||
@@ -436,6 +458,37 @@ export class AdminApiClient {
|
||||
return payload.release_versions ?? [];
|
||||
}
|
||||
|
||||
async createReleaseVersion(clusterId: string, input: CreateReleaseVersionPayload): Promise<ReleaseVersion> {
|
||||
const payload = await this.post<{ release_version: ReleaseVersion }>(`/clusters/${clusterId}/updates/releases`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
product: input.product,
|
||||
version: input.version,
|
||||
channel: input.channel || "stable",
|
||||
status: input.status || "active",
|
||||
compatibility: input.compatibility || {},
|
||||
changelog: input.changelog || "",
|
||||
artifacts: input.artifacts.map((artifact) => ({
|
||||
os: artifact.os,
|
||||
arch: artifact.arch,
|
||||
install_type: artifact.installType,
|
||||
kind: artifact.kind,
|
||||
url: artifact.url,
|
||||
sha256: artifact.sha256,
|
||||
size_bytes: artifact.sizeBytes || 0,
|
||||
metadata: artifact.metadata || {},
|
||||
})),
|
||||
});
|
||||
return payload.release_version;
|
||||
}
|
||||
|
||||
async getStaleNodeRiskReport(clusterId: string): Promise<StaleNodeRiskReport> {
|
||||
const params = new URLSearchParams({ actor_user_id: this.actorUserId });
|
||||
const payload = await this.get<{ stale_node_risk_report: StaleNodeRiskReport }>(
|
||||
`/clusters/${clusterId}/updates/stale-node-risk-report?${params.toString()}`,
|
||||
);
|
||||
return payload.stale_node_risk_report;
|
||||
}
|
||||
|
||||
async getNodeUpdatePlan(
|
||||
clusterId: string,
|
||||
nodeId: string,
|
||||
@@ -453,6 +506,14 @@ export class AdminApiClient {
|
||||
return payload.node_update_plan;
|
||||
}
|
||||
|
||||
async getNodeBridgeReplayPlan(clusterId: string, nodeId: string): Promise<NodeBridgeReplayPlan> {
|
||||
const params = new URLSearchParams({ actor_user_id: this.actorUserId });
|
||||
const payload = await this.get<{ node_bridge_replay_plan: NodeBridgeReplayPlan }>(
|
||||
`/clusters/${clusterId}/nodes/${nodeId}/updates/bridge-replay-plan?${params.toString()}`,
|
||||
);
|
||||
return payload.node_bridge_replay_plan;
|
||||
}
|
||||
|
||||
async upsertNodeUpdatePolicy(clusterId: string, nodeId: string, input: UpsertNodeUpdatePolicyPayload): Promise<NodeUpdatePolicy> {
|
||||
const payload = await this.put<{ node_update_policy: NodeUpdatePolicy }>(`/clusters/${clusterId}/nodes/${nodeId}/updates/policy`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
@@ -1269,7 +1330,7 @@ export class AdminApiClient {
|
||||
let message = `Запрос завершился ошибкой HTTP ${response.status}`;
|
||||
try {
|
||||
const payload = (await response.json()) as ApiErrorPayload;
|
||||
message = payload.error?.fallback_message || payload.error?.code || message;
|
||||
message = formatApiErrorMessage(payload, response.status) || payload.error?.fallback_message || payload.error?.code || message;
|
||||
} catch {
|
||||
// Keep generic HTTP message if backend did not return JSON.
|
||||
}
|
||||
@@ -1279,6 +1340,77 @@ export class AdminApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
function formatApiErrorMessage(payload: ApiErrorPayload, status: number) {
|
||||
const error = payload.error;
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
if (status === 409 && error.code === "conflict.legacy_compatibility_removal_is_blocked_while_stale_recovery_risk_nodes_remain") {
|
||||
const details = error.details || {};
|
||||
const parts: string[] = ["Compatibility cleanup заблокирован."];
|
||||
const blockedOperation = stringDetail(details, "blocked_operation");
|
||||
if (blockedOperation) {
|
||||
parts.push(`Операция: ${blockedOperation}.`);
|
||||
}
|
||||
const counters = [
|
||||
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, "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, "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")}` : "",
|
||||
].filter(Boolean);
|
||||
if (counters.length > 0) {
|
||||
parts.push(counters.join(" / ") + ".");
|
||||
}
|
||||
const nodeIds = arrayDetail(details, "blocked_node_ids");
|
||||
if (nodeIds.length > 0) {
|
||||
parts.push(`Blocked nodes: ${nodeIds.join(", ")}.`);
|
||||
}
|
||||
if (booleanDetail(details, "bridge_hold_required")) {
|
||||
const holdReasons = arrayDetail(details, "bridge_hold_reasons");
|
||||
const holdNodes = arrayDetail(details, "bridge_hold_node_ids");
|
||||
const holdSummary: string[] = [];
|
||||
if (holdReasons.length > 0) {
|
||||
holdSummary.push(`reasons ${holdReasons.join(", ")}`);
|
||||
}
|
||||
if (holdNodes.length > 0) {
|
||||
holdSummary.push(`nodes ${holdNodes.join(", ")}`);
|
||||
}
|
||||
parts.push(`Recovery bridge hold active${holdSummary.length > 0 ? `: ${holdSummary.join(" / ")}` : ""}.`);
|
||||
}
|
||||
const traceID = error.trace_id?.trim();
|
||||
if (traceID) {
|
||||
parts.push(`Trace: ${traceID}.`);
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function stringDetail(source: Record<string, unknown>, key: string) {
|
||||
const value = source[key];
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function numberDetail(source: Record<string, unknown>, key: string) {
|
||||
const value = source[key];
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
function booleanDetail(source: Record<string, unknown>, key: string) {
|
||||
return source[key] === true;
|
||||
}
|
||||
|
||||
function arrayDetail(source: Record<string, unknown>, key: string) {
|
||||
const value = source[key];
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0) : [];
|
||||
}
|
||||
|
||||
function browserDeviceFingerprint(): string {
|
||||
const key = "rap.webAdmin.deviceFingerprint";
|
||||
const existing = localStorage.getItem(key);
|
||||
|
||||
@@ -343,6 +343,28 @@ export type NodeUpdatePlan = {
|
||||
production_forwarding: boolean;
|
||||
};
|
||||
|
||||
export type NodeBridgeReplayProductPlan = {
|
||||
product: string;
|
||||
recovery_bridge_mode?: string;
|
||||
recovery_bridge_replay_ready: boolean;
|
||||
last_status_reason?: string;
|
||||
update_plan: NodeUpdatePlan;
|
||||
};
|
||||
|
||||
export type NodeBridgeReplayPlan = {
|
||||
schema_version: string;
|
||||
cluster_id: string;
|
||||
node_id: string;
|
||||
node_name?: string;
|
||||
health_status?: string;
|
||||
heartbeat_stale: boolean;
|
||||
bridge_hold_required: boolean;
|
||||
recovery_bridge_replay_ready: boolean;
|
||||
bridge_hold_reasons?: string[];
|
||||
bridge_actions?: string[];
|
||||
products?: NodeBridgeReplayProductPlan[];
|
||||
};
|
||||
|
||||
export type NodeUpdatePolicy = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
@@ -374,6 +396,78 @@ export type NodeUpdateStatus = {
|
||||
observed_at: string;
|
||||
};
|
||||
|
||||
export type StaleNodeRiskProduct = {
|
||||
product: string;
|
||||
current_version?: string | null;
|
||||
target_version?: string | null;
|
||||
channel?: string | null;
|
||||
strategy?: string | null;
|
||||
enabled?: boolean;
|
||||
detected_os?: string | null;
|
||||
detected_arch?: string | null;
|
||||
detected_install_type?: string | null;
|
||||
compatible_artifact_found: boolean;
|
||||
matching_release_version?: string | null;
|
||||
last_status_observed_at?: string | null;
|
||||
last_status_phase?: string | null;
|
||||
last_status_value?: string | null;
|
||||
last_status_reason?: string | null;
|
||||
recovery_bridge_required?: boolean;
|
||||
recovery_bridge_replay_ready?: boolean;
|
||||
recovery_bridge_mode?: string | null;
|
||||
risks?: string[];
|
||||
};
|
||||
|
||||
export type StaleNodeRiskNode = {
|
||||
node_id: string;
|
||||
name: string;
|
||||
node_key?: string;
|
||||
reported_version?: string | null;
|
||||
health_status: string;
|
||||
registration_status: string;
|
||||
last_seen_at?: string | null;
|
||||
heartbeat_stale: boolean;
|
||||
blocked: boolean;
|
||||
direct_peer_alert?: boolean;
|
||||
direct_peer_ready_count?: number;
|
||||
direct_peer_target_count?: number;
|
||||
direct_peer_deficit?: number;
|
||||
alerts?: string[];
|
||||
recovery_bridge_required?: boolean;
|
||||
recovery_bridge_replay_ready?: boolean;
|
||||
recovery_bridge_actions?: string[];
|
||||
risks: string[];
|
||||
products: StaleNodeRiskProduct[];
|
||||
};
|
||||
|
||||
export type StaleNodeRiskSummary = {
|
||||
total_nodes: number;
|
||||
stale_nodes: number;
|
||||
blocked_nodes: number;
|
||||
direct_peer_alert_nodes?: number;
|
||||
artifact_gap_nodes?: number;
|
||||
unknown_profile_nodes?: number;
|
||||
waiting_update_status_nodes?: number;
|
||||
unknown_version_nodes?: number;
|
||||
legacy_recovery_contract_nodes?: number;
|
||||
recovery_bridge_required_nodes?: number;
|
||||
recovery_bridge_replay_ready_nodes?: number;
|
||||
waiting_recovery_heartbeat_nodes?: number;
|
||||
};
|
||||
|
||||
export type StaleNodeRiskReport = {
|
||||
cluster_id: string;
|
||||
generated_at: string;
|
||||
heartbeat_stale_after_seconds?: number;
|
||||
legacy_removal_allowed: boolean;
|
||||
bridge_hold_required?: boolean;
|
||||
bridge_hold_node_ids?: string[];
|
||||
bridge_hold_reasons?: string[];
|
||||
blocked_operations?: string[];
|
||||
summary: StaleNodeRiskSummary;
|
||||
nodes: StaleNodeRiskNode[];
|
||||
};
|
||||
|
||||
export type MeshLink = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
@@ -1196,6 +1290,8 @@ export type NodeSyntheticMeshConfig = {
|
||||
auto_port_start?: number;
|
||||
auto_port_end?: number;
|
||||
advertise_endpoint?: string;
|
||||
advertise_endpoints?: string[];
|
||||
endpoint_candidates?: PeerEndpointCandidate[];
|
||||
advertise_transport?: string;
|
||||
connectivity_mode?: string;
|
||||
nat_type?: string;
|
||||
|
||||
Reference in New Issue
Block a user