diff --git a/docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json b/docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json
index 8acffb798..bedf32a45 100644
--- a/docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json
+++ b/docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json
@@ -299,7 +299,8 @@
"canonical_projections": [
"agent",
"vm",
- "storage"
+ "storage",
+ "network"
],
"support_floor": {
"setup": "supported",
diff --git a/docs/release-control/v6/internal/PLATFORM_SUPPORT_MODEL.md b/docs/release-control/v6/internal/PLATFORM_SUPPORT_MODEL.md
index 0676d5cd6..c929b3cce 100644
--- a/docs/release-control/v6/internal/PLATFORM_SUPPORT_MODEL.md
+++ b/docs/release-control/v6/internal/PLATFORM_SUPPORT_MODEL.md
@@ -450,7 +450,7 @@ acceptable phase-1 model for implementation and proof.
| Platform | Family | Entry point | Primary mode | Optional augmentation | Canonical projections | Admission state | Readiness stage |
| ---------------- | ------ | ------------------------- | ------------ | -------------------------------------- | ------------------------ | ---------------------------------------------- | ----------------- |
-| `vmware-vsphere` | VMware | `vCenter` only in phase 1 | `api-backed` | host or guest agent later, not phase 1 | `agent`, `vm`, `storage` | architecture locked, not yet in support matrix | `first-lab-ready` |
+| `vmware-vsphere` | VMware | `vCenter` only in phase 1 | `api-backed` | host or guest agent later, not phase 1 | `agent`, `vm`, `storage`, `network` | architecture locked, not yet in support matrix | `first-lab-ready` |
## VMware vSphere Proposed Phase-1 Floor
diff --git a/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_RESOURCE_PROJECTION_SPEC.md b/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_RESOURCE_PROJECTION_SPEC.md
index 1898bdb9d..439e0bf7b 100644
--- a/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_RESOURCE_PROJECTION_SPEC.md
+++ b/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_RESOURCE_PROJECTION_SPEC.md
@@ -25,10 +25,11 @@ Phase-1 VMware support is only valid if all of these stay true:
3. ESXi hosts project as canonical `agent`
4. virtual machines project as canonical `vm`
5. datastores project as canonical `storage`
-6. datacenter, cluster, cluster HA/DRS service state, folder, resource pool,
+6. vCenter networks project as canonical `network`
+7. datacenter, cluster, cluster HA/DRS service state, folder, resource pool,
and `vCenter` itself remain topology or relationship metadata, not
top-level Pulse resources
-7. `physical-disk`, `system-container`, `app-container`, and recovery
+8. `physical-disk`, `system-container`, `app-container`, and recovery
artifacts remain out of phase 1
## Canonical Source Contract
@@ -59,6 +60,7 @@ distinct `vCenter` environments.
| `HostSystem` / ESXi host | `agent` | yes | yes | host inventory, runtime state, health, metrics/history |
| `VirtualMachine` | `vm` | yes | yes | workload inventory, runtime state, guest identity when available, snapshot-tree visibility |
| `Datastore` | `storage` | yes | yes | inventory, capacity/free-space, accessibility, relationships |
+| `Network` | `network` | yes | yes | inventory, type, placement, host attachments, VM attachments, health signals |
| `vCenter` | none | no | no | connection and poll authority only |
| `Datacenter` | none | metadata only | no | placement and topology context |
| `ClusterComputeResource` | none | metadata only | no | placement, grouping, HA/DRS service context |
@@ -176,6 +178,30 @@ official APIs. Exact phase-1 extraction of host mounts and VM-to-datastore
usage needs live validation so Pulse does not promise more placement fidelity
than the chosen collection path can actually deliver.
+## vCenter Network To `network`
+
+What the APIs clearly support:
+
+1. `GET /api/vcenter/network` returns network identifier, name, and type
+2. VI JSON `vim.Network` exposes inventory placement through `parent`, attached
+ hosts through `host`, and attached VMs through `vm`
+3. VI JSON managed-entity signal paths can expose overall status, alarms,
+ recent tasks, and recent events for network objects when the vCenter
+ account has permission
+
+Phase-1 projection rule:
+
+1. one vCenter network becomes one canonical `network`
+2. the provider-scoped network identifier is the VMware-side primary identity
+ for that resource inside the VMware source
+3. network type, datacenter/folder placement, host attachments, and VM
+ attachments belong under the shared `vmware` facet for read-side monitoring
+4. network rows are descriptive topology and health inventory; phase 1 must not
+ introduce switch, portgroup, distributed-switch, or network-control
+ resource types
+5. network telemetry/history remains out of scope unless a later governed slice
+ proves a shared `network` metrics contract
+
## Topology And Relationship Rules
These VMware concepts remain metadata or relationships in phase 1:
@@ -186,7 +212,6 @@ These VMware concepts remain metadata or relationships in phase 1:
4. folder
5. resource pool
6. datastore cluster / storage pod
-7. network objects
Cluster HA and DRS flags are properties of the cluster placement context. They
may be rendered on canonical hosts and VMs whose placement resolves to that
@@ -197,9 +222,10 @@ That means phase-1 VMware work must not add:
1. `esxi-host`
2. `vsphere-vm`
3. `vsphere-datastore`
-4. `vsphere-cluster`
-5. `vsphere-datacenter`
-6. `vsphere-resource-pool`
+4. `vsphere-network`
+5. `vsphere-cluster`
+6. `vsphere-datacenter`
+7. `vsphere-resource-pool`
If a future slice wants one of those to become top-level, it needs a separate
governed admission decision because it would expand the shared Pulse resource
@@ -218,8 +244,8 @@ Phase-1 alert rule:
1. VMware alarm and health signals may surface only through the shared alert
and incident model
-2. alert-backed investigation must attach to canonical `agent`, `vm`, or
- `storage` resources
+2. alert-backed investigation must attach to canonical `agent`, `vm`,
+ `storage`, or `network` resources
3. cluster-, datacenter-, or folder-scoped VMware alarm context may inform the
incident, but it must not create synthetic top-level VMware incident
resources in phase 1
@@ -244,8 +270,10 @@ Phase-1 telemetry rule:
2. VM telemetry must land on the shared `vm` metrics/history path
3. datastore state or capacity-history signals must land on the shared
`storage` path
-4. phase-1 VMware work must not create a `vmware-host`, `vmware-vm`, or
- `vmware-datastore` history store
+4. vCenter network inventory may land on the shared `network` resource path,
+ but phase 1 does not claim network metrics/history
+5. phase-1 VMware work must not create a `vmware-host`, `vmware-vm`,
+ `vmware-datastore`, or `vmware-network` history store
Validation note:
@@ -277,7 +305,8 @@ The architecture is stable, but these points still require live proof:
1. exact cross-version identity floor for ESXi hosts when `host_uuid` is not
available from the chosen supported version
-2. exact relationship extraction path for VM-to-datastore usage and
+2. exact relationship extraction path for VM-to-datastore usage,
+ network-to-host attachment, network-to-VM attachment, and
datastore-to-host mount fidelity
3. exact alarm-to-canonical-resource attachment rule for cluster- or
datacenter-scoped alarms
@@ -313,3 +342,7 @@ not compensate by inventing provider-local resource types or sidecar products.
[Performance Manager Query Perf Composite](https://developer.broadcom.com/xapis/virtual-infrastructure-json-api/latest/sdk/vim25/release/PerformanceManager/moId/QueryPerfComposite/post/)
12. cluster inventory and HA/DRS service state:
[Vcenter Cluster list](https://developer.broadcom.com/xapis/vsphere-automation-api/latest/api/vcenter/cluster/get/)
+13. network inventory:
+ [Vcenter Network list](https://developer.broadcom.com/xapis/vsphere-automation-api/latest/api/vcenter/network/get/)
+14. network placement and attachments:
+ [vim.Network](https://developer.broadcom.com/xapis/virtual-storage-lifecycle-management-api/latest/vim.Network.html)
diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
index eb414297a..8cea889fa 100644
--- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
+++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
@@ -180,6 +180,15 @@ impact previews, or cross-organization sharing only through the API and
unified-resource contracts; lifecycle code must not treat that share as an
agent install target, a command-agent authority, or a reason to widen setup
tokens.
+VMware vSphere `network` resources follow that same lifecycle boundary. A
+vCenter connection may project networks into platform tables, resource
+pickers, Assistant context, or monitored-system previews through the shared
+resource contract, but those rows are provider inventory facts only. Lifecycle
+surfaces must not interpret a vSphere network as a host enrollment candidate,
+Pulse Agent install target, fleet command authority, or setup-token scope. When
+host-shaped records are coalesced for presentation, lifecycle consumers must
+use the API/unified-resource presentation result and must not create their own
+merge that bypasses registry-owned report exclusions.
The node setup modal boundary must keep guided setup and manual credential
submission separate. For new PVE/PBS setup, API Inventory and Host Telemetry
diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md
index 43db56491..1c8f7d9e3 100644
--- a/docs/release-control/v6/internal/subsystems/ai-runtime.md
+++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md
@@ -138,6 +138,13 @@ runtime cost control, and shared AI transport surfaces.
`AppLayout.tsx` and via the canonical Patrol path, and platform pages
must not replicate Patrol findings, Assistant prompts, or AI launcher
affordances inside their own chrome.
+ The vSphere Networks sub-route follows the same AI runtime boundary as the
+ vSphere overview, datastore, health, and activity routes. Network rows may
+ seed Assistant or Patrol context only as shared `network` unified-resource
+ references read through `/api/resources*` and the common handoff payloads;
+ the VMware page must not introduce VMware-local AI prompts, a provider
+ model picker, or a vSphere-specific chat/runtime route just because
+ networks are now rendered as a first-class API-native table.
## Forbidden Paths
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index 01f10952a..2242d2e39 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -1521,6 +1521,16 @@ the canonical monitored-system blocked payload.
## Current State
+VMware vSphere phase-1 inventory now reaches the product through shared API
+contracts rather than provider-local read routes. `/api/vmware/connections`
+owns saved vCenter connection health and observed counts, while canonical
+`agent`, `vm`, `storage`, and `network` resources flow through
+`/api/resources`, `/api/resources/stats`, `/api/state`, and shared Assistant
+mention payloads. Host-shaped presentation also shares the
+`ResourceRegistry.ListForPresentation` / `CoalescePresentationHostResources`
+boundary so state and resource list responses agree without bypassing
+registry-owned report exclusions.
+
TrueNAS platform-connections responses treat native VMs and network shares as
first-class observed contribution facets alongside systems, pools, datasets,
apps, disks, and recovery artifacts. The frontend TrueNAS API client must
@@ -2622,9 +2632,9 @@ implementation contract under `internal/api/vmware_handlers.go`,
`internal/api/router.go`, `internal/api/router_routes_registration.go`, and
`frontend-modern/src/api/vmware.ts`. The list response must carry one redacted
stored connection shape plus canonical `poll` health and `observed`
-contribution summary (`hosts`, `vms`, `datastores`, `viRelease`) so the shared
-settings workspace can render VMware status without another provider-local
-inventory route. When base inventory succeeds but optional signal or topology
+contribution summary (`hosts`, `vms`, `datastores`, `networks`, `viRelease`) so
+the shared settings workspace can render VMware status without another
+provider-local inventory route. When base inventory succeeds but optional signal or topology
reads degrade, that same `observed` payload must carry the canonical
partial-success shape (`degraded`, `issueCount`, summarized `issues`) instead
of collapsing the whole connection to `poll.lastError` or pretending the
@@ -2683,8 +2693,8 @@ helpers without dropping them on edit-save.
That same VMware API boundary now also owns the phase-1 runtime negative
space around inventory projection. `internal/api/router.go` may wire VMware's
supplemental ingest into the shared `/api/resources` surface so canonical
-`agent`, `vm`, and `storage` records can appear elsewhere in Pulse, but the
-public backend contract must still stop at `/api/vmware/connections*` for
+`agent`, `vm`, `storage`, and `network` records can appear elsewhere in Pulse,
+but the public backend contract must still stop at `/api/vmware/connections*` for
provider-local routes. Phase 1 must not add public `/api/vmware/resources`,
`/api/vmware/history`, `/api/vmware/alerts`, or VMware-specific recovery
transport just because the internal poller now projects VMware-backed
@@ -2693,10 +2703,12 @@ That same shared API contract now also owns Assistant mention transport for
those canonical resources. `frontend-modern/src/api/aiChat.ts`,
`internal/api/ai_handler.go`, and `internal/api/ai_handlers.go` must preserve
structured mention payloads for canonical `agent`, `vm`, `storage`, and
-`app-container` resources as shared unified-resource IDs plus shared mention
-types, so VMware-backed reads stay on `/api/ai/*` and `/api/resources*`
-instead of introducing VMware-only mention payloads or provider-local
-inventory reads under `/api/vmware/*`.
+`network` resources as shared unified-resource IDs plus shared mention types,
+so VMware-backed reads stay on `/api/ai/*` and `/api/resources*` instead of
+introducing VMware-only mention payloads or provider-local inventory reads
+under `/api/vmware/*`. Runtime-specific container/app mentions remain shared
+unified-resource mentions as well; VMware network inventory does not create a
+provider-local mention family.
That same `/api/ai/chat` payload boundary owns per-request execution-mode
overrides. Dashboard Pulse Brief and other scoped handoffs may include
`autonomous_mode:false` on the chat request to force approval-required command
@@ -4391,6 +4403,15 @@ registry-clone work on the hot path. That same governed resource contract now
also includes backend-derived `policy` and `aiSafeSummary` fields, and list,
detail, and child payloads must source those values from canonical unified
resource metadata rather than from frontend- or AI-local heuristics.
+`/api/resources`, `/api/resources/stats`, and `/api/state` also share the same
+presentation coalescing boundary for host-shaped resources. When multiple
+authoritative reports describe the same host identity, resource handlers and
+state serialization must consume `ResourceRegistry.ListForPresentation` or the
+shared `CoalescePresentationHostResources` helper rather than reimplementing a
+route-local merge. Report-merge exclusions created from canonical ingestion
+remain authoritative at that boundary, so presentation coalescing may remove
+duplicate host fragments but must not rejoin resources the registry has already
+recorded as intentionally separate.
That same resource-handler seed contract must also stay on canonical unified
resource ownership for tenant-scoped requests: once a tenant state provider
implements `UnifiedResourceSnapshotForTenant`, `/api/resources` may not fall
diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md
index ad9057a22..0f2d29343 100644
--- a/docs/release-control/v6/internal/subsystems/cloud-paid.md
+++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md
@@ -140,6 +140,12 @@ cloud-specific enforcement rules.
zero-delta and removal-only TrueNAS or VMware previews as non-consuming or
capacity-freeing changes rather than warning users that a disabled connection
still grows monitored-system usage.
+ VMware vSphere network inventory is product navigation and resource context,
+ not a separate commercial unit. The `/vmware/networks` route may display
+ API-native network rows and those rows may contribute to connection
+ previews, but cloud-paid surfaces must continue to meter only the governed
+ monitored-system grouping result. Network child-resource volume must not
+ become a hosted usage cap, upgrade prompt, or billing-admission condition.
That same shared signup boundary also owns the public privacy floor:
syntactically valid `/api/public/signup` requests resolve to one uniform
`202 Accepted` Pulse Account response whether provisioning/email side
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 2af709d9f..9e02d73e5 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -268,6 +268,14 @@ filter chip or an explicit page-owned advanced selector. Platform-owned filter
selectors must also exclude facet options from other platform scopes, even when
the underlying shared surface is mounted from the same Workloads or Storage
component.
+Platform sub-routes that add native provider inventory must stay on the shared
+platform page and table primitives. The vSphere Networks surface routes through
+`/vmware/networks`, the shared platform tab model, the command palette
+navigation model, and the canonical table/detail primitives rather than a card
+deck or VMware-local page shell. Its rows are canonical `network` resources in
+the shared reportable/resource vocabulary, so source badges, resource pickers,
+command-palette search, table chrome, and detail disclosure must all consume
+shared primitives before VMware-specific presentation logic.
Patrol's primary assessment strip is descriptive only; it must not render a
Patrol-authored recommended next step, suggested prompt chips, or a secondary
action band inside the assessment shell. If the same assessment opens
diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md
index b613442ab..c13fd6f26 100644
--- a/docs/release-control/v6/internal/subsystems/monitoring.md
+++ b/docs/release-control/v6/internal/subsystems/monitoring.md
@@ -306,9 +306,10 @@ That same VMware monitoring boundary now also includes the canonical telemetry
rule. ESXi host metrics and history belong on the shared `agent` path, VM
metrics and history belong on the shared `vm` path, and datastore
capacity/accessibility history belongs on the shared `storage` path. VMware
-phase-1 work must not create `vmware-host`, `vmware-vm`, or
-`vmware-datastore` history stores just because the collection APIs differ from
-other platforms.
+network inventory belongs on the shared `network` resource path, but phase 1
+does not claim VMware network metrics or history. VMware phase-1 work must not
+create `vmware-host`, `vmware-vm`, `vmware-datastore`, or `vmware-network`
+history stores just because the collection APIs differ from other platforms.
That same VMware monitoring boundary now also includes the source and identity
rule. Runtime collection may authenticate to `vCenter`, call multiple VMware
API families, and gather several object classes, but the emitted state must
@@ -592,19 +593,19 @@ parallel VMware event store or provider-only incident timeline.
That same VMware monitoring boundary also includes the topology-signal rule.
Signals collected from non-projected VMware topology objects such as clusters,
folders, or datacenters may inform investigation only when they can be
-attached honestly to canonical `agent`, `vm`, or `storage` resources; the
-collector must not solve that ambiguity by creating VMware-only top-level
-incident targets.
+attached honestly to canonical `agent`, `vm`, `storage`, or `network`
+resources; the collector must not solve that ambiguity by creating VMware-only
+top-level incident targets.
That same monitoring boundary now also has a concrete detail-enrichment seam.
`internal/vmware/client.go`, `internal/vmware/client_topology.go`, and
`internal/vmware/provider.go` may use the official vCenter Automation API plus
VI JSON `name`, `parent`, `runtime`, `resourcePool`, `datastore`, `host`,
-`vm`, and datastore-summary paths to enrich canonical VMware-backed resources
+`vm`, `Network.host`, `Network.vm`, and datastore-summary paths to enrich canonical VMware-backed resources
with placement, guest identity, and storage consumer context. That
enrichment remains best-effort provider detail on the shared VMware source: it
must not create a second topology cache, a VMware-only placement store, or a
-parallel guest-identity model outside the canonical `agent` / `vm` / `storage`
-resource graph.
+parallel guest-identity model outside the canonical `agent` / `vm` /
+`storage` / `network` resource graph.
The monitor adapter now also acts as the canonical bridge from live registry
rebuilds and supplemental ingest into the unified-resource timeline. That means
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 26fc414a8..050651455 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -736,7 +736,7 @@ a separate Docker-only or TrueNAS-local inventory path.
27. Keep alert-side recovery drill-ins on that same shared route-helper contract. When alert investigation surfaces such as resource-incident panels expose recovery follow-up links for TrueNAS or future API-backed platforms, they must route through the canonical `frontend-modern/src/routing/resourceLinks.ts` recovery builder instead of freezing alert-local recovery URLs or introducing another provider-shaped recovery handoff vocabulary.
28. Keep VMware onboarding runtime and recovery semantics separate on that same adjacent platform-connections contract. When `internal/api/router.go`, `internal/api/router_routes_registration.go`, or `internal/api/vmware_handlers.go` evolve VMware connection CRUD, poller-owned `poll` / `observed` summary payloads, saved-test refresh, or observed datastore/VM snapshot visibility, storage and recovery may consume the resulting shared context but must not treat those onboarding/runtime payloads as canonical recovery artifacts, restore capability, or recovery-local control transport.
29. Keep VMware datastore projection on the shared unified-resource and storage-source contracts. When `frontend-modern/src/hooks/useUnifiedResources.ts` or shared `internal/api/router.go` wiring starts surfacing VMware-backed canonical `storage` resources, storage and recovery may expose those datastores through the owned `vmware-vsphere` source/platform vocabulary for inventory, capacity, and handoff flows only; they must not reinterpret that projection as VMware recovery support, restore semantics, or a provider-local protection surface.
-30. Keep VMware placement, cluster service state, guest-detail, VM snapshot-tree, VM virtual-hardware configuration, VMware Tools, VM hardware Ethernet, and VM hardware disk enrichment descriptive on that same shared unified-resource contract. When `internal/vmware/provider.go`, `internal/unifiedresources/types.go`, and `frontend-modern/src/hooks/useUnifiedResources.ts` project datacenter, cluster, `vmware.clusterHaEnabled`, `vmware.clusterDrsEnabled`, folder, runtime-host, datastore-attachment, guest-hostname, guest-IP, `vmware.currentSnapshotId`, `vmware.snapshotTree`, snapshot creation/state/quiesce/current markers, child snapshot metadata, `vmware.hardware`, virtual hardware version, hardware upgrade policy/version/status/error, boot type/order/retry/setup-mode flags, CPU cores-per-socket and hot-add/remove flags, memory hot-add settings, `vmware.tools`, Tools run state, version status, version number/string, install type, upgrade policy, auto-update support, install-attempt count, guest reboot requests, `vmware.networkAdapters`, adapter MAC address/type, backing network id/name, backing type, connection state, start-connected / guest-control flags, `vmware.virtualDisks`, virtual disk label/type, IDE/SCSI/SATA/NVMe placement, VMDK path, backing type, datastore name, or capacity onto canonical VMware `agent` / `vm` / `storage` resources, storage and recovery may use that detail for labeling, navigation, and VM investigation context only; they must not promote those topology, cluster-service, guest, snapshot-tree, virtual-hardware, VMware Tools, vNIC, or virtual disk fields into recovery ownership, restore targeting, protection grouping, compliance scoring, or a VMware-local recovery taxonomy without a separately governed slice.
+30. Keep VMware placement, cluster service state, guest-detail, VM snapshot-tree, VM virtual-hardware configuration, VMware Tools, VM hardware Ethernet, VM hardware disk, and network enrichment descriptive on that same shared unified-resource contract. When `internal/vmware/provider.go`, `internal/unifiedresources/types.go`, and `frontend-modern/src/hooks/useUnifiedResources.ts` project datacenter, cluster, `vmware.clusterHaEnabled`, `vmware.clusterDrsEnabled`, folder, runtime-host, datastore-attachment, guest-hostname, guest-IP, `vmware.currentSnapshotId`, `vmware.snapshotTree`, snapshot creation/state/quiesce/current markers, child snapshot metadata, `vmware.hardware`, virtual hardware version, hardware upgrade policy/version/status/error, boot type/order/retry/setup-mode flags, CPU cores-per-socket and hot-add/remove flags, memory hot-add settings, `vmware.tools`, Tools run state, version status, version number/string, install type, upgrade policy, auto-update support, install-attempt count, guest reboot requests, `vmware.networkAdapters`, adapter MAC address/type, backing network id/name, backing type, connection state, start-connected / guest-control flags, `vmware.virtualDisks`, virtual disk label/type, IDE/SCSI/SATA/NVMe placement, VMDK path, backing type, datastore name, capacity, `vmware.networkType`, `vmware.networkHostNames`, or `vmware.networkVmNames` onto canonical VMware `agent` / `vm` / `storage` / `network` resources, storage and recovery may use that detail for labeling, navigation, and VM investigation context only; they must not promote those topology, cluster-service, guest, snapshot-tree, virtual-hardware, VMware Tools, vNIC, virtual disk, or network fields into recovery ownership, restore targeting, protection grouping, compliance scoring, or a VMware-local recovery taxonomy without a separately governed slice.
31. Keep VMware datastore classification neutral on the shared storage adapter contract. When `frontend-modern/src/features/storageBackups/resourceStorageMapping.ts`, `frontend-modern/src/features/storageBackups/resourceStoragePresentation.ts`, and `frontend-modern/src/features/storageBackups/storageAdapters.ts` evolve canonical storage-record mapping, VMware-backed datastores must continue to land on the shared storage route as inventory-only datastores with neutral protection fallback, not as backup repositories, backup targets, or recovery-protected resources.
That same shared storage adapter boundary also owns canonical platform
family vocabulary through the governed platform manifest.
@@ -3157,18 +3157,19 @@ Storage and recovery must not infer VMware restore support, recovery rollups,
or VMware-local protection semantics from the presence of those datastores or
VM snapshot-read context on the shared pages.
That same shared adapter floor also now carries richer VMware placement,
-cluster-service, guest-detail, VM virtual-hardware, and VMware Tools metadata through the
-canonical `agent` / `vm` / `storage` resources that storage and recovery can
-inspect on shared pages.
+cluster-service, guest-detail, VM virtual-hardware, VMware Tools, and network
+metadata through the canonical `agent` / `vm` / `storage` / `network`
+resources that storage and recovery can inspect on shared pages.
`internal/vmware/provider.go`, `internal/unifiedresources/types.go`, and
`frontend-modern/src/hooks/useUnifiedResources.ts` may expose datacenter,
cluster, cluster HA/DRS service state, folder, runtime-host,
datastore-attachment, guest-hostname, and guest-IP detail plus VM
virtual-hardware version, boot, CPU/memory hot-add, VMware Tools run-state,
-version, policy, install-attempt, error, and guest-reboot context as inventory
-context, but those fields stay descriptive only. Storage and recovery must not
-treat topology labels, cluster-service flags, datastore attachments, guest
-identity, virtual-hardware posture, or VMware Tools posture as recovery
+version, policy, install-attempt, error, guest-reboot context, and network
+attachment context as inventory context, but those fields stay descriptive
+only. Storage and recovery must not treat topology labels, cluster-service
+flags, datastore attachments, guest identity, network attachments,
+virtual-hardware posture, or VMware Tools posture as recovery
ownership, restore targeting, protection grouping, or a new VMware-local
storage/recovery taxonomy until a separately governed slice explicitly promotes
them into recovery contracts.
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index 2c9ffc477..e34328816 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -562,7 +562,7 @@ AI-only summary payloads, or page-local heuristics.
8. Keep provider-backed signal metadata on shared canonical resource fields.
VMware status, alarm, task, and snapshot signals must flow through shared
`vmware` metadata plus shared `resource-incident` timeline entries on
- canonical `agent`, `vm`, and `storage` resources instead of creating
+ canonical `agent`, `vm`, `storage`, and `network` resources instead of creating
provider-only resource kinds, identities, or history schemas.
9. Keep summary-surface emphasis on canonical resource IDs. Infrastructure
summary row-hover, chart-hover, and route-focus behavior must keep using the
@@ -1001,9 +1001,9 @@ That same VMware contract now also includes the shared source boundary. When
runtime work starts, VMware-backed records must flow through one canonical
VMware source key plus `platformType: vmware-vsphere`, not through separate
`vcenter` and `esxi` source forks or provider-local raw type aliases. One
-host, VM, or datastore from VMware should therefore still look like one shared
-Pulse `agent`, `vm`, or `storage` resource to downstream selectors, drawers,
-alerts, AI, and route filters.
+host, VM, datastore, or network from VMware should therefore still look like
+one shared Pulse `agent`, `vm`, `storage`, or `network` resource to downstream
+selectors, drawers, alerts, AI, and route filters.
That shared source boundary now also has a concrete frontend/runtime adapter
floor. `internal/unifiedresources/types.go`, `internal/unifiedresources/registry.go`,
`internal/unifiedresources/views.go`, `frontend-modern/src/hooks/useUnifiedResources.ts`,
@@ -1139,14 +1139,15 @@ VM `instance_uuid` / `bios_uuid` and host UUID when available belong under the
shared canonical identity model for future merge or assistant reasoning, not
inside a VMware-only dedupe lane.
That same VMware contract now also includes the topology rule. `vCenter`,
-datacenter, cluster, folder, resource pool, datastore cluster, and network
-objects may enrich canonical `agent`, `vm`, and `storage` resources as
+datacenter, cluster, folder, resource pool, and datastore cluster objects may
+enrich canonical `agent`, `vm`, `storage`, and `network` resources as
placement metadata or relationships, but they must not appear as synthetic
top-level VMware resource types just to mirror the upstream inventory tree.
Snapshot trees and VMware alarm/event/task context are also governed by that
-same rule: they may enrich canonical `vm`, `agent`, or `storage` resources and
-their timelines, but they do not become shared recovery artifacts, new
-resource kinds, or a parallel VMware incident model.
+same rule: they may enrich canonical `vm`, `agent`, `storage`, or `network`
+resources and their timelines, but they do not become shared recovery
+artifacts, new provider-local resource kinds, or a parallel VMware incident
+model.
That same topology contract now also has a concrete projection seam.
`internal/vmware/provider.go` must preserve VMware placement and identity
detail on the shared `vmware` facet only: hosts may carry datacenter,
@@ -1158,7 +1159,10 @@ Ethernet adapter plus VM hardware disk metadata plus canonical parentage to
the owning ESXi `agent`; datastores may
carry datacenter/folder placement plus shared storage-node and workload
consumer metadata through `storage.nodes`, `storage.consumerCount`, and
-`storage.topConsumers`. VMs may also carry VI JSON snapshot-tree context under
+`storage.topConsumers`; networks may carry network type, datacenter/folder
+placement, host attachments, VM attachments, and VMware health/task/event
+signal summaries under the shared `vmware` facet on canonical `network`
+resources. VMs may also carry VI JSON snapshot-tree context under
`vmware.currentSnapshotId` and `vmware.snapshotTree`, including snapshot
managed-object reference, display name, description, creation time, power
state, quiesce flag, current marker, replay support, and child snapshots.
@@ -1191,9 +1195,9 @@ Cluster HA and DRS state belongs under `vmware.clusterHaEnabled` and
cluster. It is API-native monitoring context from the vCenter cluster summary,
not a synthetic cluster resource, lifecycle command surface, scheduling policy
model, or recovery/protection signal.
-Those enrichments must remain subordinate to shared `agent`, `vm`, and
-`storage` resources rather than becoming a VMware-only topology graph, recovery
-artifact, canonical identity alias, or separate provider detail drawer
+Those enrichments must remain subordinate to shared `agent`, `vm`, `storage`,
+and `network` resources rather than becoming a VMware-only topology graph,
+recovery artifact, canonical identity alias, or separate provider detail drawer
contract.
TrueNAS disk telemetry now follows the same rule. API-backed TrueNAS disks must
populate canonical `physicalDisk.temperature` and reuse the shared
diff --git a/frontend-modern/src/AppLayout.tsx b/frontend-modern/src/AppLayout.tsx
index ce38ff52f..19b5fa984 100644
--- a/frontend-modern/src/AppLayout.tsx
+++ b/frontend-modern/src/AppLayout.tsx
@@ -376,7 +376,7 @@ export function AppLayout(props: AppLayoutProps) {
label: 'vSphere',
route: ROOT_VMWARE_PATH,
settingsRoute: '/settings/infrastructure',
- tooltip: 'VMware vSphere hosts, virtual machines, and datastores',
+ tooltip: 'VMware vSphere hosts, virtual machines, datastores, and networks',
enabled: isVisible('vmware'),
live: isVisible('vmware'),
icon: CpuIcon,
diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts
index 5ac121685..4cb118998 100644
--- a/frontend-modern/src/__tests__/App.architecture.test.ts
+++ b/frontend-modern/src/__tests__/App.architecture.test.ts
@@ -50,14 +50,16 @@ describe('App architecture', () => {
);
expect(appSource).toContain("import('./components/Workloads/WorkloadsSurface')");
expect(appSource).toContain("import('./components/Storage/Storage')");
+ expect(appSource).toContain("import('./components/Recovery/Recovery')");
expect(appSource).toContain(
- "import('./components/Recovery/Recovery')",
+ '',
);
- expect(appSource).toContain('');
expect(appSource).toContain('');
expect(appSource).toContain('');
expect(appSource).toContain('');
- expect(appSource).toContain('} />');
+ expect(appSource).toContain(
+ '} />',
+ );
expect(appSource).toContain('await preloadRouteModule(route);');
expect(appRuntimeStateSource).not.toContain('preloadLazyRoutes');
expect(appRuntimeStateSource).not.toContain("import('@/pages/Alerts')");
@@ -82,6 +84,9 @@ describe('App architecture', () => {
expect(appLayoutSource).toContain("id: 'kubernetes',");
expect(appLayoutSource).toContain("id: 'truenas',");
expect(appLayoutSource).toContain("id: 'vmware',");
+ expect(appLayoutSource).toContain(
+ "tooltip: 'VMware vSphere hosts, virtual machines, datastores, and networks'",
+ );
// Governed platform/runtime primary nav: Infrastructure / Workloads /
// Storage / Recovery are not duplicated as equal primary tab
// entries, and the Docker / Podman route is presented as the Containers
diff --git a/frontend-modern/src/api/__tests__/resourcesHotPath.guardrails.test.ts b/frontend-modern/src/api/__tests__/resourcesHotPath.guardrails.test.ts
index 4b4df068c..6fd02ea29 100644
--- a/frontend-modern/src/api/__tests__/resourcesHotPath.guardrails.test.ts
+++ b/frontend-modern/src/api/__tests__/resourcesHotPath.guardrails.test.ts
@@ -2,15 +2,23 @@ import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
-const resourcesHandlerSource = readFileSync(resolve(process.cwd(), '../internal/api/resources.go'), 'utf8');
+const resourcesHandlerSource = readFileSync(
+ resolve(process.cwd(), '../internal/api/resources.go'),
+ 'utf8',
+);
describe('resource API hot-path guardrails', () => {
it('reuses one registry snapshot per request when deriving canonical by-type aggregations', () => {
expect(resourcesHandlerSource.match(/allResources := registry\.List\(\)/g) ?? []).toHaveLength(
- 2,
+ 0,
);
expect(
- resourcesHandlerSource.match(/computeResourceContractByType\(allResources\)/g) ?? [],
+ resourcesHandlerSource.match(
+ /allResources := presentationResourcesFromRegistry\(registry\)/g,
+ ) ?? [],
+ ).toHaveLength(2);
+ expect(
+ resourcesHandlerSource.match(/computeResourceContractStats\(allResources\)/g) ?? [],
).toHaveLength(2);
expect(resourcesHandlerSource).not.toContain('computeResourceContractByType(registry.List())');
});
diff --git a/frontend-modern/src/api/__tests__/vmware.test.ts b/frontend-modern/src/api/__tests__/vmware.test.ts
index 2cc597139..47dc404a0 100644
--- a/frontend-modern/src/api/__tests__/vmware.test.ts
+++ b/frontend-modern/src/api/__tests__/vmware.test.ts
@@ -33,6 +33,7 @@ describe('VMwareAPI', () => {
hosts: 3,
vms: 42,
datastores: 6,
+ networks: 8,
viRelease: ' 8.0.3 ',
degraded: true,
issueCount: 3,
@@ -70,6 +71,7 @@ describe('VMwareAPI', () => {
hosts: 3,
vms: 42,
datastores: 6,
+ networks: 8,
viRelease: '8.0.3',
degraded: true,
issueCount: 3,
diff --git a/frontend-modern/src/api/vmware.ts b/frontend-modern/src/api/vmware.ts
index 4e0e94593..f7526ee29 100644
--- a/frontend-modern/src/api/vmware.ts
+++ b/frontend-modern/src/api/vmware.ts
@@ -41,6 +41,7 @@ export interface VMwareConnectionObservedSummary {
hosts: number;
vms: number;
datastores: number;
+ networks: number;
viRelease?: string;
degraded?: boolean;
issueCount?: number;
@@ -120,6 +121,7 @@ const normalizeVMwareConnectionObservedSummary = (
hosts: finiteNumberOrUndefined(observed.hosts) ?? 0,
vms: finiteNumberOrUndefined(observed.vms) ?? 0,
datastores: finiteNumberOrUndefined(observed.datastores) ?? 0,
+ networks: finiteNumberOrUndefined(observed.networks) ?? 0,
viRelease: optionalTrimmedString(observed.viRelease),
degraded: strictBoolean(observed.degraded),
issueCount: finiteNumberOrUndefined(observed.issueCount),
@@ -162,9 +164,7 @@ const serializeVMwareConnectionInput = (input: VMwareConnectionInput) => ({
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
...(input.monitorVms !== undefined ? { monitorVms: input.monitorVms } : {}),
...(input.monitorHosts !== undefined ? { monitorHosts: input.monitorHosts } : {}),
- ...(input.monitorDatastores !== undefined
- ? { monitorDatastores: input.monitorDatastores }
- : {}),
+ ...(input.monitorDatastores !== undefined ? { monitorDatastores: input.monitorDatastores } : {}),
});
export const isRedactedVMwareSecret = (value: string | null | undefined) =>
diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts
index e86bacba3..a8b441397 100644
--- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts
+++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts
@@ -33,6 +33,35 @@ describe('resourceDetailDrawerVmwareModel', () => {
]);
});
+ it('surfaces vCenter network resources as read-only topology context', () => {
+ const vmware: ResourceVMwareMeta = {
+ connectionName: 'Lab VC',
+ entityType: 'network',
+ overallStatus: 'yellow',
+ networkType: 'DISTRIBUTED_PORTGROUP',
+ datacenterName: 'Primary DC',
+ folderName: 'Networks',
+ networkHostNames: ['esxi-01.lab.local', 'esxi-02.lab.local'],
+ networkVmNames: ['warehouse-api-01', 'etl-batch-01'],
+ activeAlarmCount: 1,
+ activeAlarmSummary: 'Network uplink redundancy (yellow)',
+ };
+
+ expect(buildVMwareDetailsSummary('network', vmware)).toBe(
+ 'Lab VC · Read-only vCenter context · 2 hosts · 2 VMs · 1 alarm',
+ );
+
+ const sections = buildVMwareDetailSections('network', vmware);
+ expect(sections.find((section) => section.id === 'state')?.rows).toContainEqual({
+ label: 'Network type',
+ value: 'Distributed Portgroup',
+ });
+ expect(sections.find((section) => section.id === 'network')?.rows).toEqual([
+ { label: 'Hosts', value: 'esxi-01.lab.local, esxi-02.lab.local' },
+ { label: 'VMs', value: 'warehouse-api-01, etl-batch-01' },
+ ]);
+ });
+
it('surfaces vSphere snapshot trees as read-only VM detail context', () => {
const vmware: ResourceVMwareMeta = {
connectionName: 'Lab VC',
diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts
index 3f3529ba8..0a25f87ee 100644
--- a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts
+++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts
@@ -38,6 +38,9 @@ const asTrimmedString = (value?: string | null): string => (value || '').trim();
const formatCount = (count: number, label: string): string =>
`${count} ${label}${count === 1 ? '' : 's'}`;
+const summarizeList = (values: string[] | undefined): string =>
+ (values ?? []).map(asTrimmedString).filter(Boolean).join(', ');
+
const formatBoolLabel = (value?: boolean): string => {
if (value === undefined) return '';
return value ? 'Yes' : 'No';
@@ -483,6 +486,8 @@ const vmwareEntityLabel = (entityType?: string): string => {
return 'VM';
case 'datastore':
return 'Datastore';
+ case 'network':
+ return 'Network';
default:
return asTrimmedString(entityType);
}
@@ -540,6 +545,12 @@ export const buildVMwareDetailsSummary = (
if (resourceType === 'vm' && virtualDiskCount > 0) {
parts.push(formatCount(virtualDiskCount, 'disk'));
}
+ if (resourceType === 'network') {
+ const hostCount = vmware.networkHostNames?.length ?? vmware.networkHostIds?.length ?? 0;
+ const vmCount = vmware.networkVmNames?.length ?? vmware.networkVmIds?.length ?? 0;
+ if (hostCount > 0) parts.push(formatCount(hostCount, 'host'));
+ if (vmCount > 0) parts.push(formatCount(vmCount, 'VM'));
+ }
const hardware = resourceType === 'vm' ? hardwareSummary(vmware.hardware) : '';
if (hardware) {
parts.push(hardware);
@@ -610,6 +621,10 @@ export const buildVMwareDetailSections = (
value: asTrimmedString(vmware.maintenanceMode),
tone: getWarningTone(Boolean(asTrimmedString(vmware.maintenanceMode))),
},
+ {
+ label: 'Network type',
+ value: formatEnumLabel(vmware.networkType),
+ },
]);
const placementRows = filterNonEmptyRows([
@@ -678,7 +693,19 @@ export const buildVMwareDetailSections = (
},
]);
- const networkRows = resourceType === 'vm' ? networkAdapterRows(vmware.networkAdapters) : [];
+ const networkRows =
+ resourceType === 'vm'
+ ? networkAdapterRows(vmware.networkAdapters)
+ : filterNonEmptyRows([
+ {
+ label: 'Hosts',
+ value: summarizeList(vmware.networkHostNames),
+ },
+ {
+ label: 'VMs',
+ value: summarizeList(vmware.networkVmNames),
+ },
+ ]);
const vmwareHardwareRows = resourceType === 'vm' ? hardwareRows(vmware) : [];
const vmwareToolsRows = resourceType === 'vm' ? toolsRows(vmware.tools) : [];
const diskRows = resourceType === 'vm' ? virtualDiskRows(vmware.virtualDisks) : [];
diff --git a/frontend-modern/src/components/Settings/__tests__/reportingResourceTypes.test.ts b/frontend-modern/src/components/Settings/__tests__/reportingResourceTypes.test.ts
index ecf8f3f45..8f2f0e418 100644
--- a/frontend-modern/src/components/Settings/__tests__/reportingResourceTypes.test.ts
+++ b/frontend-modern/src/components/Settings/__tests__/reportingResourceTypes.test.ts
@@ -10,6 +10,7 @@ describe('toReportingResourceType', () => {
expect(toReportingResourceType('docker-host')).toBe('docker-host');
expect(toReportingResourceType('network-endpoint')).toBe('network-endpoint');
expect(toReportingResourceType('storage')).toBe('storage');
+ expect(toReportingResourceType('network')).toBe('network');
});
it('adapts kubernetes resource kinds to the current reporting API token at the edge', () => {
diff --git a/frontend-modern/src/components/shared/__tests__/CommandPaletteModal.test.tsx b/frontend-modern/src/components/shared/__tests__/CommandPaletteModal.test.tsx
index a94c7174e..5ecb0ff11 100644
--- a/frontend-modern/src/components/shared/__tests__/CommandPaletteModal.test.tsx
+++ b/frontend-modern/src/components/shared/__tests__/CommandPaletteModal.test.tsx
@@ -39,6 +39,7 @@ const infrastructureVisibility = () =>
makeResource({ id: 'pve-1', type: 'agent', platformType: 'proxmox-pve' }),
makeResource({ id: 'docker-1', type: 'docker-host', platformType: 'docker' }),
makeResource({ id: 'k8s-1', type: 'k8s-cluster', platformType: 'kubernetes' }),
+ makeResource({ id: 'vc-1', type: 'network', platformType: 'vmware-vsphere' }),
]);
describe('CommandPaletteModal', () => {
@@ -74,13 +75,14 @@ describe('CommandPaletteModal', () => {
expect(commandPaletteModelSource).toContain("id: 'nav-kubernetes'");
expect(commandPaletteModelSource).toContain("id: 'nav-truenas'");
expect(commandPaletteModelSource).toContain("id: 'nav-vmware'");
+ expect(commandPaletteModelSource).toContain("id: 'nav-vmware-networks'");
expect(commandPaletteModelSource).not.toContain("id: 'nav-infrastructure'");
expect(commandPaletteModelSource).not.toContain("id: 'nav-workloads'");
expect(commandPaletteModelSource).not.toContain("id: 'nav-storage'");
expect(commandPaletteModelSource).not.toContain("id: 'nav-recovery'");
});
- it('renders the platform entries, container runtime lens, and dedicated Kubernetes pods command', () => {
+ it('renders platform entries, runtime lens commands, and vSphere network inventory', () => {
render(() => (
{
expect(screen.getByText('Go to Containers')).toBeInTheDocument();
expect(screen.getByText('Go to Kubernetes Pods')).toBeInTheDocument();
expect(screen.getByText('/kubernetes/pods')).toBeInTheDocument();
+ expect(screen.getByText('Go to vSphere')).toBeInTheDocument();
+ expect(screen.getByText('Go to vSphere Networks')).toBeInTheDocument();
+ expect(screen.getByText('/vmware/networks')).toBeInTheDocument();
});
it('navigates to the Kubernetes pods sub-tab', async () => {
@@ -111,6 +116,22 @@ describe('CommandPaletteModal', () => {
expect(onClose).toHaveBeenCalledTimes(1);
});
+ it('navigates to the vSphere networks sub-tab', async () => {
+ const onClose = vi.fn();
+ render(() => (
+
+ ));
+
+ await fireEvent.click(screen.getByText('Go to vSphere Networks'));
+
+ expect(navigateMock).toHaveBeenCalledWith('/vmware/networks');
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
it('uses the shared search input and keeps Enter selection behavior', async () => {
const onClose = vi.fn();
render(() => (
@@ -147,5 +168,6 @@ describe('CommandPaletteModal', () => {
expect(screen.queryByText('Go to Kubernetes')).not.toBeInTheDocument();
expect(screen.queryByText('Go to TrueNAS')).not.toBeInTheDocument();
expect(screen.queryByText('Go to vSphere')).not.toBeInTheDocument();
+ expect(screen.queryByText('Go to vSphere Networks')).not.toBeInTheDocument();
});
});
diff --git a/frontend-modern/src/components/shared/commandPaletteModel.ts b/frontend-modern/src/components/shared/commandPaletteModel.ts
index 5bc6e2320..8fc8620e9 100644
--- a/frontend-modern/src/components/shared/commandPaletteModel.ts
+++ b/frontend-modern/src/components/shared/commandPaletteModel.ts
@@ -25,6 +25,7 @@ export type CommandPaletteCommandPaths = {
kubernetesPodsPath: string;
trueNasPath: string;
vmwarePath: string;
+ vmwareNetworksPath: string;
};
export function buildCommandPaletteCommands(options: {
@@ -88,14 +89,23 @@ export function buildCommandPaletteCommands(options: {
}
if (primaryInfrastructureNavigationIsVisible(options.infrastructureVisibility, 'vmware')) {
- commands.push({
- id: 'nav-vmware',
- label: 'Go to vSphere',
- description: options.paths.vmwarePath,
- shortcut: 'g v',
- keywords: ['vmware', 'vsphere', 'esxi', 'vms', 'datastores'],
- action: () => options.navigate(options.paths.vmwarePath),
- });
+ commands.push(
+ {
+ id: 'nav-vmware',
+ label: 'Go to vSphere',
+ description: options.paths.vmwarePath,
+ shortcut: 'g v',
+ keywords: ['vmware', 'vsphere', 'esxi', 'vms', 'datastores', 'networks'],
+ action: () => options.navigate(options.paths.vmwarePath),
+ },
+ {
+ id: 'nav-vmware-networks',
+ label: 'Go to vSphere Networks',
+ description: options.paths.vmwareNetworksPath,
+ keywords: ['vmware', 'vsphere', 'esxi', 'networks', 'portgroups'],
+ action: () => options.navigate(options.paths.vmwareNetworksPath),
+ },
+ );
}
commands.push(
diff --git a/frontend-modern/src/components/shared/useCommandPaletteState.ts b/frontend-modern/src/components/shared/useCommandPaletteState.ts
index d4723480e..3db532373 100644
--- a/frontend-modern/src/components/shared/useCommandPaletteState.ts
+++ b/frontend-modern/src/components/shared/useCommandPaletteState.ts
@@ -30,6 +30,7 @@ export function useCommandPaletteState(props: CommandPaletteModalProps) {
kubernetesPodsPath: buildKubernetesPath('pods'),
trueNasPath: buildTrueNASPath(),
vmwarePath: buildVmwarePath(),
+ vmwareNetworksPath: buildVmwarePath('networks'),
},
infrastructureVisibility: props.infrastructureVisibility(),
navigate,
diff --git a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts
index 28b223b25..3f105d3b4 100644
--- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts
+++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts
@@ -203,6 +203,7 @@ describe('platform overview layout guardrails', () => {
expect(vmwarePageSurfaceSource).toContain(' {
resource.vmware?.clusterName,
formatVmwareClusterServices(resource.vmware),
resource.vmware?.datastoreNames?.join(' '),
+ resource.vmware?.networkType,
+ resource.vmware?.networkHostNames?.join(' '),
+ resource.vmware?.networkVmNames?.join(' '),
...(resource.tags ?? []),
]
.filter((value): value is string => typeof value === 'string')
diff --git a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx
index 005b62aa6..69d63aa17 100644
--- a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx
+++ b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx
@@ -19,12 +19,14 @@ import {
import { VsphereAlertsTable } from './VsphereAlertsTable';
import { VsphereActivityTable } from './VsphereActivityTable';
import { VsphereDatastoresTable } from './VsphereDatastoresTable';
+import { VsphereNetworksTable } from './VsphereNetworksTable';
import { VsphereVirtualMachinesTable } from './VsphereVirtualMachinesTable';
// vSphere phase 1 projects ESXi hosts as canonical `agent`, virtual machines
-// as canonical `vm`, and datastores as canonical `storage`; provider-native
-// topology stays in VMware metadata under those shared resources.
-const VMWARE_RESOURCE_QUERY = 'type=agent,vm,storage';
+// as canonical `vm`, datastores as canonical `storage`, and vCenter networks
+// as canonical `network`; provider-native topology stays in VMware metadata
+// under those shared resources.
+const VMWARE_RESOURCE_QUERY = 'type=agent,vm,storage,network';
const VALID_TABS = new Set(VMWARE_TAB_SPECS.map((tab) => tab.id));
const vmwareIcon = () => ;
@@ -103,6 +105,15 @@ export function VmwarePageSurface() {
emptyDescription="Datastores appear here once the vCenter connection enumerates them."
/>
+
+
+ [] = [
+ { value: 'all', label: 'All' },
+ { value: 'healthy', label: 'Healthy', tone: 'success' },
+ { value: 'attention', label: 'Attention', tone: 'warning' },
+ { value: 'unknown', label: 'Unknown' },
+];
+
+const networkName = (resource: Resource): string =>
+ asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
+
+const networkType = (resource: Resource): string =>
+ asTrimmedString(resource.vmware?.networkType) || '-';
+
+const compactList = (values: Array): string[] =>
+ values.map((value) => asTrimmedString(value)).filter((value): value is string => Boolean(value));
+
+const summarizeValues = (
+ values: string[],
+ empty = '-',
+ visibleCount = 2,
+): { label: string; title: string } => {
+ if (values.length === 0) return { label: empty, title: '' };
+ const visible = values.slice(0, visibleCount);
+ const suffix = values.length > visible.length ? ` +${values.length - visible.length}` : '';
+ return { label: `${visible.join(', ')}${suffix}`, title: values.join(', ') };
+};
+
+const hostSummary = (resource: Resource): { label: string; title: string } =>
+ summarizeValues(compactList(resource.vmware?.networkHostNames ?? []), '-', 2);
+
+const vmSummary = (resource: Resource): { label: string; title: string } =>
+ summarizeValues(compactList(resource.vmware?.networkVmNames ?? []), '-', 2);
+
+const vmCount = (resource: Resource): number =>
+ resource.vmware?.networkVmNames?.length ?? resource.vmware?.networkVmIds?.length ?? 0;
+
+const statusLabel = (resource: Resource): string => {
+ switch (mapVmwareNetworkStatus(resource)) {
+ case 'healthy':
+ return 'Healthy';
+ case 'attention':
+ return 'Attention';
+ case 'unknown':
+ return 'Unknown';
+ }
+};
+
+const statusPillClass = (resource: Resource): string => {
+ switch (mapVmwareNetworkStatus(resource)) {
+ case 'healthy':
+ return 'border-emerald-300/50 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
+ case 'attention':
+ return 'border-amber-300/50 bg-amber-500/10 text-amber-700 dark:text-amber-300';
+ case 'unknown':
+ return 'border-border bg-surface-alt text-muted';
+ }
+};
+
+const StatusPill: Component<{ resource: Resource }> = (props) => (
+
+ {statusLabel(props.resource)}
+
+);
+
+export const VsphereNetworksTable: Component<{
+ networks: Resource[];
+ scope: Resource[];
+ emptyIcon: JSX.Element;
+ emptyTitle: string;
+ emptyDescription: string;
+ showToolbar?: boolean;
+}> = (props) => {
+ const tableState = createPlatformTableFilterState({
+ resources: () => props.networks,
+ initialStatus: 'all' as VmwareNetworkStatusFilter,
+ filter: filterVmwareNetworks,
+ });
+ const drawer = createPlatformResourceDetailState({ idPrefix: 'vsphere-network-drawer' });
+ const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.scope);
+
+ return (
+ 0}
+ fallback={
+
+ }
+ >
+