diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 261024dd3..134b282c3 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -71,14 +71,15 @@ shared filter rail must remove that route value along with the other active facets; the feature must not replace the shared rail with a page-local select. Large-estate platform pages must keep one canonical inventory snapshot for the -initial read and explicit refresh path. The Proxmox overview owns its unified -resource request and passes that snapshot into the shared workloads state; the -workloads adapter may map the snapshot into the legacy guest boundary, but must -not issue a second workload inventory request or create a second infrastructure -poll. Refreshing the overview must invalidate that owner snapshot and update -both the node and guest regions from the same result, so a large estate cannot -render contradictory counts or pay duplicate transport and reconciliation -costs. +initial read and explicit refresh path. The Proxmox and VMware vSphere +overviews own their source-scoped unified-resource requests and pass those +snapshots into the shared workloads state; the workloads adapter may map the +snapshot into the legacy guest boundary, but must not issue a second workload +inventory request or create a second infrastructure poll. Refreshing an +overview must invalidate that owner snapshot and update both the host/node and +guest regions from the same result, so a large estate cannot render +contradictory counts, flash a false empty workload state, or pay duplicate +transport and reconciliation costs. The overview's bounded structural summary remains ahead of its long virtualized inventory at desktop and narrow widths. In particular, Proxmox must show up to six phone rows or twelve larger-layout rows before the guest list instead of diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 8dd2c243f..85620b329 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1360,6 +1360,19 @@ an interim unknown or never-backed-up state while waiting for a WebSocket tick. This correction adds no request, subscription, retry, or resource scan; `useUnifiedResources.test.ts` and `useWorkloads.test.ts` pin the boundary. +### vSphere owner snapshots avoid duplicate estate enumeration + +The VMware vSphere overview owns one `type=agent,vm,storage,network` resource +query scoped to `source=vmware-vsphere` and passes the resulting canonical +snapshot into its embedded Workloads state. Host and VM regions therefore +paint from one paged generation and explicit refresh path; the Workloads owner +must not launch the generic all-platform workload pagination or its generic +infrastructure snapshot. The shared unified-resource hook projects the same +type-and-source filter from canonical WebSocket state, so this request +reduction adds no polling fallback or freshness regression. Contract tests pin +the request shape, realtime projection, snapshot injection, and refresh +handoff. + ### Command-session liveness lookup stays bounded and in-memory The connections ledger's command-channel liveness check diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index a5c34064a..d58ad27d3 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -25,6 +25,15 @@ The shared `internal/api/` helper-binary download route is isolated from storage and recovery semantics. `/download/pulse-agent-helper` serves a signed Linux runtime artifact only; it cannot read, mutate, restore, export, or select storage state, recovery timelines, backup providers, or repository scope. +The parallel `/download/pulse-agent-runner` artifact route and +`/api/agents/action-runner/credential` issuance route are also adjacent +API/agent-lifecycle boundaries. A signed runner binary or host-bound typed +action credential is not a backup, snapshot, recovery point, restore grant, +retention decision, or repository selector. Safe-profile inspect/apply/rollback +preserves collector identity and installation files only; it must not rewrite +storage/recovery evidence or reinterpret a restored legacy collector profile as +recovery success. Typed `host.storage_cleanup` remains governed remediation, +not storage-recovery authority inferred from the credential itself. First-run authentication always writes the canonical `.env` persistence artifact before runtime state changes. Root systemd installation may also @@ -53,6 +62,14 @@ workload or infrastructure inventory request. Backup and recovery tables keep their domain-specific evidence requests, while an overview refresh must update all overview regions from the same owner snapshot rather than mixing reads from different inventory generations. +The vSphere overview follows the same read-side ownership rule for its host, +VM, datastore, and network regions. Its canonical snapshot is constrained to +the `vmware-vsphere` source at the resource API boundary, and the embedded +workloads adapter consumes that page-owned snapshot instead of issuing a +second generic inventory request. Source aliases used to project a canonical +websocket snapshot are transport filtering only: a VMware datastore remains +descriptive inventory and does not gain backup, protection, recovery-point, +or restore semantics from that projection. The physical-disks surface consumes every page in the server-declared unified resource result, scopes same-named Proxmox nodes by instance, and never hides a disk behind a client-side page ceiling. Search includes vendor, WWN, transport, diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 1187cfe1a..80f8a1280 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -2046,6 +2046,14 @@ without waiting for a later WebSocket merge; REST and WebSocket delivery must not disagree about already-authoritative fields. The boundary is pinned by `useUnifiedResources.test.ts` and `useWorkloads.test.ts`. +Source-scoped platform owners remain canonical realtime consumers. The shared +frontend hook locally projects supported `type` plus `source` filters from the +canonical WebSocket snapshot, including the public `vmware-vsphere` alias for +the raw `vmware` source, rather than degrading a source-scoped page to periodic +REST refreshes. VMware Overview passes that same source-scoped snapshot into +the embedded Workloads state, so hosts and VMs share one inventory generation +and one explicit refresh path. + ### Agent libvirt domains use a provider-neutral VM facet The registry projects a host's validated libvirt domains as `vm` resources diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 884e8c841..992ab22c7 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,31 +1,23 @@ { "version": 1, - "base_sha": "6453593b4b984eaa7d1d40727f43e12528212dd7", - "verified_at": "2026-08-29T21:11:53Z", + "base_sha": "6d4ee48000f9266d161af000f88d9d1f10042328", + "verified_at": "2026-08-29T22:35:09Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/api/agentDiagnostics.ts", - "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx", - "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx", - "frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts", - "frontend-modern/src/components/Settings/useNodeModalState.ts" + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx", + "frontend-modern/src/hooks/useUnifiedResources.ts" ], "content_sha256": { - "frontend-modern/src/api/agentDiagnostics.ts": "0bf0f3d0854ebd03ab9d80ccef0e39655aeeb2bd16d18901b527ba3955a086bb", - "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "0fe6f4cb3f10adbf13306eaa73b390e24b4d586ec71479cb7c8494ce158e6302", - "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx": "5aa559093039bc18785b4df88c2c955d67465b969b85979bc2a836a7d64d6dd0", - "frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts": "d352500b9f804111fe07602aa3716e9d1ff1a30ac5fe3b1d6a7d0587c36c6171", - "frontend-modern/src/components/Settings/useNodeModalState.ts": "0f198074f88ef8d55a0fdc5142bacee4a39b572b12f94f7ed88a0fa10724c067" + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "659851ff4111f53d15f74d7959ab9e3f21683eecf0e5c55d91e477f47ad93540", + "frontend-modern/src/hooks/useUnifiedResources.ts": "0b5cca6992ae17575e5f689268e152cbdd47f61b3169043cec314b27c11d8c97" }, "routes": [ - "/settings/infrastructure?add=docker", - "/settings/infrastructure?add=pve", - "/settings/infrastructure/agent-doctor" + "/vmware/overview" ], "viewports": [ { - "width": 1440, - "height": 1000 + "width": 1280, + "height": 720 }, { "width": 390, @@ -33,15 +25,14 @@ } ], "states": [ - "mock-backed Docker installer before token generation, including the transitional Docker-in-LXC trust notice", - "Proxmox VE onboarding with the API-first strategy selected", - "Proxmox VE onboarding with Host Telemetry Agent selected and monitoring-only credential guidance visible", - "Agent Doctor route with the current fleet diagnostics loaded" + "vSphere overview with seven ESXi hosts and eighteen VMs loaded from one source-scoped canonical inventory snapshot", + "desktop host table, workload filters, grouped VM table, and live-connection status", + "narrow host and VM table projections with the responsive filter control and fixed mobile navigation" ], "interactions": [ - "opened the Docker onboarding deep link and verified monitoring-first copy, token authorization guidance, and install handoff at desktop width", - "narrowed the Docker onboarding dialog to 390 pixels and verified the title, Docker-in-LXC notice, and token action remained visible", - "opened Proxmox VE onboarding, switched from Connect via API to Host Telemetry Agent, and verified the monitoring-only command-authority boundary copy", - "opened Agent Doctor and verified the routed diagnostics surface loaded without console-blocking UI failures" + "reloaded the vSphere overview with browser request tracing enabled and verified the only resource-list request was the page-owned type-and-source-scoped snapshot", + "verified the embedded VM filter and table populated from the owner snapshot without a generic workload or infrastructure resource request", + "set a 390 by 844 viewport and verified the host rows, VM search, Filters action, grouped VM rows, and mobile navigation remained visible and usable", + "reset the temporary viewport override to the browser default after responsive verification" ] } diff --git a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx index 914af894e..908ca3df5 100644 --- a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx +++ b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx @@ -40,6 +40,7 @@ import { } from '@/components/Workloads/workloadMetricHistoryModel'; import { usePersistentSignal } from '@/hooks/usePersistentSignal'; import { STORAGE_KEYS } from '@/utils/localStorage'; +import type { Resource } from '@/types/resource'; import { VsphereHostsTable } from './VsphereHostsTable'; import { VMWARE_TAB_SPECS, @@ -57,7 +58,7 @@ import { VsphereNetworksTable } from './VsphereNetworksTable'; // 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 VMWARE_RESOURCE_QUERY = 'type=agent,vm,storage,network&source=vmware-vsphere'; const VALID_TABS = new Set(VMWARE_TAB_SPECS.map((tab) => tab.id)); const VMWARE_PLATFORM_FILTER = 'vmware-vsphere'; @@ -276,6 +277,10 @@ export function VmwarePageSurface() { metricHistoryRange={metricHistoryRange} setMetricHistoryRange={setMetricHistoryRange} inventorySourcesQuery={inventorySources} + resourceSnapshot={() => + loading() && model().resources.length === 0 ? undefined : model().resources + } + resourceSnapshotRefetch={() => refetch()} /> @@ -348,6 +353,8 @@ interface VmwareOverviewProps { metricHistoryRange: Accessor; setMetricHistoryRange: (value: WorkloadTableMetricHistoryRange) => void; inventorySourcesQuery: WorkloadsInventorySourcesQuery; + resourceSnapshot: Accessor; + resourceSnapshotRefetch: () => Promise; } function VmwareOverview(props: VmwareOverviewProps) { @@ -356,6 +363,8 @@ function VmwareOverview(props: VmwareOverviewProps) { containers: [], nodes: [], useWorkloads: true, + resourceSnapshot: props.resourceSnapshot, + resourceSnapshotRefetch: props.resourceSnapshotRefetch, forcedPlatform: VMWARE_PLATFORM_FILTER, forcedViewMode: 'vm', suppressPlatformFilter: true, diff --git a/frontend-modern/src/features/vmware/__tests__/VmwarePageSurface.contract.test.tsx b/frontend-modern/src/features/vmware/__tests__/VmwarePageSurface.contract.test.tsx index 9b7033b33..0e0fc3e42 100644 --- a/frontend-modern/src/features/vmware/__tests__/VmwarePageSurface.contract.test.tsx +++ b/frontend-modern/src/features/vmware/__tests__/VmwarePageSurface.contract.test.tsx @@ -8,6 +8,7 @@ const mockPathname = vi.hoisted(() => vi.fn(() => '/vmware/overview')); const mockVersionInfo = vi.hoisted(() => vi.fn()); const mockGetGlobalTimeline = vi.hoisted(() => vi.fn()); const mockListInventorySources = vi.hoisted(() => vi.fn()); +const mockUseWorkloadsState = vi.hoisted(() => vi.fn()); const makeResource = (resource: Partial & Pick): Resource => ({ @@ -21,13 +22,18 @@ const makeResource = (resource: Partial & Pick { +const setResources = ( + resources: Resource[], + options: { loading?: boolean; refetch?: ReturnType } = {}, +) => { + const refetch = options.refetch ?? vi.fn().mockResolvedValue(resources); mockUseUnifiedResources.mockReturnValue({ resources: () => resources, - loading: () => false, + loading: () => options.loading ?? false, error: () => null, - refetch: vi.fn(), + refetch, }); + return refetch; }; vi.mock('@/hooks/useUnifiedResources', () => ({ @@ -73,13 +79,7 @@ vi.mock('@/components/Workloads/WorkloadsSurface', () => ({ })); vi.mock('@/components/Workloads/useWorkloadsState', () => ({ - useWorkloadsState: () => ({ - surfaceConnected: () => false, - surfaceInitialDataReceived: () => false, - allGuests: () => [], - search: () => '', - setSearch: vi.fn(), - }), + useWorkloadsState: (...args: unknown[]) => mockUseWorkloadsState(...args), })); vi.mock('@/features/platformPage/sharedPlatformPage', () => ({ @@ -126,6 +126,13 @@ describe('VmwarePageSurface contract', () => { mockVersionInfo.mockReturnValue(null); mockGetGlobalTimeline.mockResolvedValue({ recentChanges: [] }); mockListInventorySources.mockResolvedValue({ sources: [] }); + mockUseWorkloadsState.mockReturnValue({ + surfaceConnected: () => false, + surfaceInitialDataReceived: () => false, + allGuests: () => [], + search: () => '', + setSearch: vi.fn(), + }); setResources([]); }); @@ -171,6 +178,49 @@ describe('VmwarePageSurface contract', () => { ); }); + it('reuses the source-scoped page snapshot for the vSphere workload surface', async () => { + const refetch = vi.fn().mockResolvedValue([]); + const resources = [ + makeResource({ + id: 'esxi-host-1', + type: 'agent', + vmware: { entityType: 'host', managedObjectId: 'host-1' }, + }), + makeResource({ + id: 'vm-app-01', + type: 'vm', + vmware: { entityType: 'vm', managedObjectId: 'vm-1' }, + }), + makeResource({ + id: 'other-platform-vm', + type: 'vm', + platformType: 'proxmox-pve', + vmware: undefined, + }), + ]; + setResources(resources, { refetch }); + + render(() => ); + + expect(mockUseUnifiedResources).toHaveBeenCalledWith( + expect.objectContaining({ + query: 'type=agent,vm,storage,network&source=vmware-vsphere', + cacheKey: 'vmware-workspace', + }), + ); + const stateOptions = mockUseWorkloadsState.mock.calls[0]?.[0] as { + resourceSnapshot: () => Resource[] | undefined; + resourceSnapshotRefetch: () => Promise; + }; + expect(stateOptions.resourceSnapshot()?.map((resource) => resource.id)).toEqual([ + 'esxi-host-1', + 'vm-app-01', + ]); + + await stateOptions.resourceSnapshotRefetch(); + expect(refetch).toHaveBeenCalledTimes(1); + }); + it('does not treat vSphere ESXi API host resources as Pulse agent update targets', () => { mockVersionInfo.mockReturnValue({ version: 'v6.0.0-rc.6', diff --git a/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts b/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts index b69041cba..cf9dcf63a 100644 --- a/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts +++ b/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts @@ -663,6 +663,96 @@ describe('useUnifiedResources', () => { dispose(); }); + it('keeps source filters in the canonical resource request', async () => { + let dispose = () => {}; + let result: ReturnType | undefined; + createRoot((d) => { + dispose = d; + result = useUnifiedResources({ + query: 'type=agent,vm,storage,network&source=vmware-vsphere', + cacheKey: 'vmware-overview-contract', + }); + }); + + await flushAsync(); + expect(apiFetchMock).toHaveBeenCalledTimes(1); + expect(apiFetchMock).toHaveBeenNthCalledWith( + 1, + '/api/resources?type=agent%2Cvm%2Cstorage%2Cnetwork&source=vmware-vsphere&page=1&limit=100', + { cache: 'no-store' }, + ); + await waitForResourceCount(() => result!.resources().length); + + dispose(); + }); + + it('projects source-scoped queries from the canonical websocket snapshot', async () => { + const vmwareVM = createWsResource({ + id: 'vmware-vm-1', + type: 'vm', + name: 'vmware-vm-1', + displayName: 'vmware-vm-1', + platformId: 'vmware-vm-1', + platformType: 'vmware-vsphere', + sourceType: 'api', + sources: ['vmware'], + }); + const proxmoxVM = createWsResource({ + id: 'proxmox-vm-1', + type: 'vm', + name: 'proxmox-vm-1', + displayName: 'proxmox-vm-1', + platformId: 'proxmox-vm-1', + platformType: 'proxmox-pve', + sourceType: 'api', + sources: ['proxmox'], + }); + batch(() => { + setWsState('resources', [vmwareVM, proxmoxVM]); + setWsState('lastUpdate', 1738843200100); + setWsResourceChange({ + version: 1738843200100, + changedIds: new Set([vmwareVM.id, proxmoxVM.id]), + }); + }); + + let dispose = () => {}; + let result: ReturnType | undefined; + createRoot((d) => { + dispose = d; + result = useUnifiedResources({ + query: 'type=agent,vm&source=vmware-vsphere', + cacheKey: 'vmware-websocket-projection', + initialHydration: 'prefer-ws', + }); + }); + + await waitForValue(() => result!.resources().length, 1); + expect(result!.resources().map((resource) => resource.id)).toEqual(['vmware-vm-1']); + expect(apiFetchMock).not.toHaveBeenCalled(); + + dispose(); + }); + + it('keeps unsupported source filters on the canonical REST fallback path', async () => { + let dispose = () => {}; + let result: ReturnType | undefined; + createRoot((d) => { + dispose = d; + result = useUnifiedResources({ + query: 'source=future-provider', + cacheKey: 'unsupported-source-filter', + initialHydration: 'prefer-ws', + }); + }); + + await flushAsync(); + expect(apiFetchMock).toHaveBeenCalledTimes(1); + await waitForResourceCount(() => result!.resources().length); + + dispose(); + }); + it('loads physical disks beyond the first resource page without dropping wide-node inventory', async () => { const firstPage = Array.from({ length: 100 }, (_, index) => ({ ...v2Resource, diff --git a/frontend-modern/src/hooks/useUnifiedResources.ts b/frontend-modern/src/hooks/useUnifiedResources.ts index 3a6c77e9e..51fde6cc5 100644 --- a/frontend-modern/src/hooks/useUnifiedResources.ts +++ b/frontend-modern/src/hooks/useUnifiedResources.ts @@ -1130,7 +1130,36 @@ const enrichCanonicalAllResourcesCache = ( setUnifiedResourcesCache(entry, enriched); }; -const parseUnifiedResourcesTypeFilter = (query: string): Set | null => { +type UnifiedResourcesLocalFilter = { + types: Set; + sources: Set; +}; + +const normalizeUnifiedResourceSource = (source: string): string => { + const normalized = source.trim().toLowerCase(); + switch (normalized) { + case 'k8s': + return 'kubernetes'; + case 'vmware-vsphere': + return 'vmware'; + default: + return normalized; + } +}; + +const SUPPORTED_UNIFIED_RESOURCE_SOURCES = new Set([ + 'proxmox', + 'agent', + 'docker', + 'pbs', + 'pmg', + 'kubernetes', + 'truenas', + 'vmware', + 'availability', +]); + +const parseUnifiedResourcesLocalFilter = (query: string): UnifiedResourcesLocalFilter | null => { const normalizedQuery = normalizeUnifiedResourcesQuery(query); if (normalizedQuery === '') { return null; @@ -1138,9 +1167,10 @@ const parseUnifiedResourcesTypeFilter = (query: string): Set | nul const params = new URLSearchParams(normalizedQuery); const types = new Set(); + const sources = new Set(); for (const [key, value] of params.entries()) { - if (key !== 'type') { + if (key !== 'type' && key !== 'source') { return null; } @@ -1149,26 +1179,50 @@ const parseUnifiedResourcesTypeFilter = (query: string): Set | nul .map((candidate) => asTrimmedString(candidate)) .filter((candidate): candidate is string => candidate !== undefined) .forEach((candidate) => { - types.add(resolveType(candidate)); + if (key === 'type') { + types.add(resolveType(candidate)); + return; + } + const normalizedSource = normalizeUnifiedResourceSource(candidate); + if (SUPPORTED_UNIFIED_RESOURCE_SOURCES.has(normalizedSource)) { + sources.add(normalizedSource); + } }); } - return types.size > 0 ? types : null; + return types.size > 0 || sources.size > 0 ? { types, sources } : null; }; const filterCanonicalUnifiedResources = ( resources: Resource[], query: string, - typeFilter: Set | null, + localFilter: UnifiedResourcesLocalFilter | null, ): Resource[] | null => { const normalizedQuery = normalizeUnifiedResourcesQuery(query); if (normalizedQuery === '') { return resources; } - if (!typeFilter) { + if (!localFilter) { return null; } - return resources.filter((resource) => typeFilter.has(resolveType(resource.type))); + return resources.filter((resource) => { + if (localFilter.types.size > 0 && !localFilter.types.has(resolveType(resource.type))) { + return false; + } + if (localFilter.sources.size === 0) { + return true; + } + const platformSources = resource.platformData?.sources; + const resourceSources = [ + ...(resource.sources ?? []), + ...(Array.isArray(platformSources) + ? platformSources.filter((source): source is string => typeof source === 'string') + : []), + ]; + return resourceSources.some((source) => + localFilter.sources.has(normalizeUnifiedResourceSource(source)), + ); + }); }; /** @@ -1213,8 +1267,8 @@ const seedUnifiedResourcesCacheFromAllResources = ( return entry; } - const typeFilter = parseUnifiedResourcesTypeFilter(query); - if (!typeFilter) { + const localFilter = parseUnifiedResourcesLocalFilter(query); + if (!localFilter) { return entry; } @@ -1225,9 +1279,8 @@ const seedUnifiedResourcesCacheFromAllResources = ( return entry; } - entry.resources = allResourcesEntry.resources.filter((resource) => - typeFilter.has(resolveType(resource.type)), - ); + entry.resources = + filterCanonicalUnifiedResources(allResourcesEntry.resources, query, localFilter) ?? []; entry.policyPosture = allResourcesEntry.policyPosture; entry.aggregations = allResourcesEntry.aggregations; entry.hasSnapshot = true; @@ -1370,8 +1423,8 @@ export function useUnifiedResources(options?: UseUnifiedResourcesOptions) { const initialHydration = options?.initialHydration ?? 'immediate'; const enabled = options?.enabled ?? (() => true); const realtimeEnabled = options?.realtimeEnabled ?? enabled; - const typeFilter = parseUnifiedResourcesTypeFilter(query); - const supportsCanonicalWsHydration = query === '' || typeFilter !== null; + const localFilter = parseUnifiedResourcesLocalFilter(query); + const supportsCanonicalWsHydration = query === '' || localFilter !== null; const prefersWsInitialHydration = (initialHydration === 'prefer-ws' || initialHydration === 'prefer-ws-then-rest') && supportsCanonicalWsHydration; @@ -1763,7 +1816,7 @@ export function useUnifiedResources(options?: UseUnifiedResourcesOptions) { const projectedResources = filterCanonicalUnifiedResources( mergedWsResources, query, - typeFilter, + localFilter, ); const now = Date.now(); clearInitialHydrationTimeout();