diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
index e80756ebd..b74e461b8 100644
--- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
+++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx
@@ -23,7 +23,6 @@ import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import { PlatformOutdatedAgentNotice } from '@/features/platformPage/PlatformOutdatedAgentNotice';
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
import { STORAGE_KEYS } from '@/utils/localStorage';
-import { resourceMatchesSearch } from '@/utils/resourceSearchMatch';
import {
PlatformErrorState,
PlatformSectionTabs,
@@ -41,6 +40,7 @@ import {
PROXMOX_TAB_SPECS,
buildProxmoxPageModel,
buildVisibleProxmoxTabSpecs,
+ filterProxmoxNodesForSearch,
type ProxmoxPageModel,
type ProxmoxPageTabId,
} from './proxmoxPageModel';
@@ -247,11 +247,13 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) {
workloadsState.surfaceInitialDataReceived() &&
workloadsState.allGuests().length > 0,
);
- const filteredNodes = createMemo(() => {
- const term = workloadsState.search();
- if (!term.trim()) return props.model().pveNodes;
- return props.model().pveNodes.filter((node) => resourceMatchesSearch(node, term));
- });
+ const filteredNodes = createMemo(() =>
+ filterProxmoxNodesForSearch(
+ props.model().pveNodes,
+ props.model().guests,
+ workloadsState.search(),
+ ),
+ );
return (
diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts
index ff9f8664a..5f2fd436d 100644
--- a/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts
+++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts
@@ -4,6 +4,7 @@ import {
PROXMOX_TAB_SPECS,
buildProxmoxPageModel,
buildVisibleProxmoxTabSpecs,
+ filterProxmoxNodesForSearch,
getResourceVersion,
resolveProxmoxPlatformScope,
} from '../proxmoxPageModel';
@@ -31,6 +32,44 @@ describe('proxmoxPageModel', () => {
]);
});
+ describe('filterProxmoxNodesForSearch', () => {
+ const minipc = makeResource({
+ id: 'minipc',
+ type: 'agent',
+ proxmox: { nodeName: 'minipc', clusterName: 'homelab' },
+ });
+ const delly = makeResource({
+ id: 'delly',
+ type: 'agent',
+ proxmox: { nodeName: 'delly', clusterName: 'homelab' },
+ });
+ const debianGo = makeResource({
+ id: 'system-container-112',
+ type: 'system-container',
+ name: 'debian-go',
+ proxmox: { vmid: 112, nodeName: 'minipc' },
+ });
+
+ it('returns every node when the search term is empty', () => {
+ expect(filterProxmoxNodesForSearch([minipc, delly], [debianGo], '')).toEqual([
+ minipc,
+ delly,
+ ]);
+ });
+
+ it('keeps the host node of a matching guest so a guest search does not empty the nodes table', () => {
+ // Regression: searching a guest name used to filter the nodes table to
+ // nothing, surfacing the "No Proxmox VE nodes" empty state even though the
+ // guest's host node exists and the guest is listed below.
+ const result = filterProxmoxNodesForSearch([minipc, delly], [debianGo], 'debian-go');
+ expect(result).toEqual([minipc]);
+ });
+
+ it('still matches a node by its own name', () => {
+ expect(filterProxmoxNodesForSearch([minipc, delly], [debianGo], 'delly')).toEqual([delly]);
+ });
+ });
+
it('builds a Proxmox-first estate model from canonical v6 resources', () => {
const model = buildProxmoxPageModel([
makeResource({
diff --git a/frontend-modern/src/features/proxmox/proxmoxPageModel.ts b/frontend-modern/src/features/proxmox/proxmoxPageModel.ts
index a67cdfc71..5212d109a 100644
--- a/frontend-modern/src/features/proxmox/proxmoxPageModel.ts
+++ b/frontend-modern/src/features/proxmox/proxmoxPageModel.ts
@@ -1,6 +1,7 @@
import type { Resource, ResourceMetric, ResourceType } from '@/types/resource';
import type { ResourceChange } from '@/types/resource';
import { formatProxmoxVersion } from '@/utils/proxmoxVersion';
+import { resourceMatchesSearch } from '@/utils/resourceSearchMatch';
export type ProxmoxPageTabId = 'overview' | 'storage' | 'replication' | 'backups' | 'ceph' | 'mail';
@@ -175,6 +176,32 @@ export function getResourceNodeName(resource: Resource): string {
);
}
+// filterProxmoxNodesForSearch narrows the nodes table to match the shared
+// workload search box. Because that box is a VM/LXC filter, filtering nodes by
+// the raw term alone collapses the table to its empty state whenever the term
+// matches a guest but not a node name — which misreads as "no Proxmox nodes"
+// while the matching guest is listed right below. Keep a node when it matches
+// the term directly OR when it hosts a guest that matches, so a guest search
+// still shows that guest's host node for context.
+export function filterProxmoxNodesForSearch(
+ nodes: Resource[],
+ guests: Resource[],
+ term: string,
+): Resource[] {
+ if (!term.trim()) return nodes;
+ const matchingGuestNodeNames = new Set(
+ guests
+ .filter((guest) => resourceMatchesSearch(guest, term))
+ .map((guest) => getResourceNodeName(guest))
+ .filter(Boolean),
+ );
+ return nodes.filter(
+ (node) =>
+ resourceMatchesSearch(node, term) ||
+ matchingGuestNodeNames.has(getResourceNodeName(node)),
+ );
+}
+
export function getResourceVmid(resource: Resource): string {
const vmid = resource.proxmox?.vmid;
if (typeof vmid === 'number' && Number.isFinite(vmid)) {