feat(manager): expose safe VPS edge health
This commit is contained in:
@@ -439,6 +439,12 @@ export function createLocalPreviewDeviceCore({ fixture = null } = {}) {
|
||||
displayName: input.displayName,
|
||||
deploymentRef: input.deploymentRef ?? null,
|
||||
lifecycleState: input.lifecycleState ?? "provisioning",
|
||||
channel: existing?.channel ?? {
|
||||
lifecycleState: "disabled",
|
||||
generationRef: null,
|
||||
runtimeState: "disabled",
|
||||
lastErrorCode: null,
|
||||
},
|
||||
createdAt: existing?.createdAt || now(),
|
||||
updatedAt: now(),
|
||||
};
|
||||
@@ -716,6 +722,12 @@ function seedArusnaviB2Preview({
|
||||
displayName: "Preview VPS edge",
|
||||
deploymentRef: "deployment:preview",
|
||||
lifecycleState: "active",
|
||||
channel: {
|
||||
lifecycleState: "active",
|
||||
generationRef: "channel-generation:preview",
|
||||
runtimeState: "accepted",
|
||||
lastErrorCode: null,
|
||||
},
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
export type ControlViewId =
|
||||
| "catalog"
|
||||
| "infrastructure"
|
||||
| "hosts"
|
||||
| "sessions"
|
||||
| "bindings"
|
||||
| "commands"
|
||||
@@ -145,6 +146,13 @@ export function DeviceControlView({
|
||||
}))}
|
||||
/>
|
||||
) : null}
|
||||
{view === "hosts" ? (
|
||||
<HostsView
|
||||
workspace={workspace}
|
||||
canManage={platformOwner}
|
||||
onCreateEdge={() => setDialog("edge")}
|
||||
/>
|
||||
) : null}
|
||||
{view === "sessions" ? <SessionsView workspace={workspace} /> : null}
|
||||
{view === "bindings" ? (
|
||||
<BindingsView
|
||||
@@ -372,6 +380,50 @@ function InfrastructureView({ workspace, canManageCatalog, canManageRoutes, onCr
|
||||
);
|
||||
}
|
||||
|
||||
function HostsView({ workspace, canManage, onCreateEdge }: {
|
||||
workspace: ProjectWorkspace;
|
||||
canManage: boolean;
|
||||
onCreateEdge: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ControlStack>
|
||||
<ControlToolbar
|
||||
copy="VPS-хосты показаны через зарегистрированную роль Edge: состояние берётся из pinned mTLS Core↔Edge канала, а связи — из маршрутов проекта. Адреса, ключи и credentials в браузер не выдаются."
|
||||
actions={canManage ? <Button size="compact" variant="primary" onClick={onCreateEdge}>Новый VPS Edge</Button> : null}
|
||||
/>
|
||||
<ControlSection title="VPS и Edge-хосты" count={workspace.edges.length}>
|
||||
<ResourceGrid empty="VPS/Edge-хосты для проекта пока не зарегистрированы.">
|
||||
{workspace.edges.map((edge) => {
|
||||
const routes = workspace.routes.filter((route) => route.edgeRef === edge.edgeRef);
|
||||
const runtimeState = edge.channel?.runtimeState ?? "unobserved";
|
||||
return (
|
||||
<ResourceCard
|
||||
key={edge.edgeRef}
|
||||
eyebrow="VPS / EDGE HOST"
|
||||
title={edge.displayName}
|
||||
description={edge.deploymentRef || edge.edgeKey}
|
||||
status={runtimeState}
|
||||
meta={[
|
||||
`registration · ${edge.lifecycleState}`,
|
||||
`channel · ${edge.channel?.lifecycleState ?? "disabled"}`,
|
||||
`${routes.length} ${routes.length === 1 ? "маршрут" : "маршрутов"}`,
|
||||
...(edge.channel?.generationRef ? [edge.channel.generationRef] : []),
|
||||
...(edge.channel?.lastErrorCode ? [`error · ${edge.channel.lastErrorCode}`] : []),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ResourceGrid>
|
||||
</ControlSection>
|
||||
<GlassSurface padding="md" tone="soft">
|
||||
<p className="device-manager-card-copy">
|
||||
Общий реестр произвольных VPS, deployments, services и управляемая консоль требуют отдельного канонического ontology package. Текущий экран намеренно отображает только уже существующую проверяемую Edge-инфраструктуру.
|
||||
</p>
|
||||
</GlassSurface>
|
||||
</ControlStack>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionsView({ workspace }: { workspace: ProjectWorkspace }) {
|
||||
return (
|
||||
<ControlStack>
|
||||
@@ -931,9 +983,9 @@ function commaList(value: string) {
|
||||
}
|
||||
|
||||
function statusTone(status: string): "neutral" | "success" | "warning" | "danger" {
|
||||
if (["active", "online", "verified", "applied", "recorded", "immutable"].includes(status)) return "success";
|
||||
if (["failed", "rejected", "revoked", "retired"].includes(status)) return "danger";
|
||||
if (["draft", "provisioning", "pending", "pending_external_approval", "unknown", "disabled"].includes(status)) return "warning";
|
||||
if (["active", "accepted", "online", "verified", "applied", "recorded", "immutable"].includes(status)) return "success";
|
||||
if (["absent", "failed", "rejected", "revoked", "retired"].includes(status)) return "danger";
|
||||
if (["connecting", "draft", "provisioning", "pending", "pending_external_approval", "unknown", "unobserved", "disabled"].includes(status)) return "warning";
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ const sectionNavigation: Record<PrimarySection, NavigationItem[]> = {
|
||||
{ id: "collections", label: "Коллекции", icon: "folder", capability: "inventory.read" },
|
||||
],
|
||||
infrastructure: [
|
||||
{ id: "hosts", label: "VPS и хосты", icon: "building", capability: "telemetry.observe" },
|
||||
{ id: "infrastructure", label: "Edges и маршруты", icon: "globe", capability: "telemetry.observe" },
|
||||
{ id: "catalog", label: "Модели и адаптеры", icon: "database", capability: "project.read" },
|
||||
],
|
||||
@@ -799,7 +800,7 @@ function ProjectView({ view, workspace, canManageCollections, canClaim, canConfi
|
||||
onInventoryDetailChange: (detail: DeviceInventoryDetailState | null) => void;
|
||||
}) {
|
||||
if (!workspace) return <div className="device-manager-panel-empty">Загружаем проект…</div>;
|
||||
if (["catalog", "infrastructure", "sessions", "bindings", "commands", "audit", "access", "settings"].includes(view)) {
|
||||
if (["catalog", "hosts", "infrastructure", "sessions", "bindings", "commands", "audit", "access", "settings"].includes(view)) {
|
||||
return <DeviceControlView
|
||||
view={view as ControlViewId}
|
||||
workspace={workspace}
|
||||
@@ -1455,6 +1456,7 @@ function viewTitle(view: ViewId) {
|
||||
inventory: "Устройства",
|
||||
collections: "Коллекции",
|
||||
catalog: "Модели и адаптеры",
|
||||
hosts: "VPS и хосты",
|
||||
infrastructure: "Edges и маршруты",
|
||||
sessions: "Gateway sessions",
|
||||
bindings: "Data bindings",
|
||||
@@ -1468,7 +1470,7 @@ function viewTitle(view: ViewId) {
|
||||
function sectionForView(view: ViewId): PrimarySection {
|
||||
if (view === "overview") return "overview";
|
||||
if (view === "inventory" || view === "collections" || view === "sessions") return "devices";
|
||||
if (view === "catalog" || view === "infrastructure") return "infrastructure";
|
||||
if (view === "catalog" || view === "hosts" || view === "infrastructure") return "infrastructure";
|
||||
if (view === "bindings" || view === "commands" || view === "settings") return "management";
|
||||
return "administration";
|
||||
}
|
||||
|
||||
@@ -86,10 +86,15 @@
|
||||
.device-control-command-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr) auto;
|
||||
align-items: end;
|
||||
align-items: start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.device-control-command-form > .nodedc-select-anchor,
|
||||
.device-control-command-form > .nodedc-button {
|
||||
margin-top: 1.35rem;
|
||||
}
|
||||
|
||||
.device-control-command-policy p {
|
||||
margin-top: 5px;
|
||||
color: var(--nodedc-text-secondary);
|
||||
@@ -145,6 +150,11 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.device-control-command-form > .nodedc-select-anchor,
|
||||
.device-control-command-form > .nodedc-button {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.device-control-command-policy > .nodedc-status {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
@@ -526,6 +536,8 @@ body {
|
||||
min-height: 220px;
|
||||
place-items: center;
|
||||
color: var(--nodedc-text-secondary);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.55;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +194,12 @@ export interface EdgeView {
|
||||
displayName: string;
|
||||
deploymentRef: string | null;
|
||||
lifecycleState: string;
|
||||
channel: {
|
||||
lifecycleState: string;
|
||||
generationRef: string | null;
|
||||
runtimeState: "accepted" | "connecting" | "absent" | "unobserved" | "disabled" | "revoked" | string;
|
||||
lastErrorCode: string | null;
|
||||
};
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user