diff --git a/.gemini/docs/unified-resource-architecture.md b/.gemini/docs/unified-resource-architecture.md
index b7ce652..3c820ee 100644
--- a/.gemini/docs/unified-resource-architecture.md
+++ b/.gemini/docs/unified-resource-architecture.md
@@ -547,9 +547,9 @@ The dedicated `/resources` unified view was abandoned in favor of migrating exis
4. **Cleanup (Phase 6) - COMPLETED:**
- [x] Remove debug console.log statements from frontend routes
- [x] Remove legacy fallback code from route components (Docker, Hosts, Dashboard)
+ - [x] Simplify route components to use centralized `useResourcesAsLegacy()` hook
- [ ] Remove unused legacy arrays from backend StateFrontend (optional - still broadcast for API compatibility)
- [ ] Remove legacy AI context fallback (optional - verify AI uses unified model first)
- - [ ] Simplify route components to use resources directly (remove adapter layer)
---
diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx
index 9de9e1d..d7a14ea 100644
--- a/frontend-modern/src/App.tsx
+++ b/frontend-modern/src/App.tsx
@@ -47,6 +47,7 @@ import { UpdateProgressModal } from './components/UpdateProgressModal';
import type { UpdateStatus } from './api/updates';
import { AIChat } from './components/AI/AIChat';
import { aiChatStore } from './stores/aiChat';
+import { useResourcesAsLegacy } from './hooks/useResources';
const Dashboard = lazy(() =>
import('./components/Dashboard/Dashboard').then((module) => ({ default: module.Dashboard })),
@@ -92,169 +93,29 @@ export const useDarkMode = () => {
return context;
};
-// Docker route component that properly uses activeAlerts from useWebSocket
+// Docker route component - uses unified resources via useResourcesAsLegacy hook
function DockerRoute() {
const wsContext = useContext(WebSocketContext);
if (!wsContext) {
return
Loading...
;
}
- const { state, activeAlerts } = wsContext;
+ const { activeAlerts } = wsContext;
+ const { asDockerHosts } = useResourcesAsLegacy();
- // Use unified resources if available, fall back to legacy state.dockerHosts
- const hosts = createMemo(() => {
- const dockerHostResources = state.resources?.filter(r => r.type === 'docker-host') ?? [];
- const dockerContainerResources = state.resources?.filter(r => r.type === 'docker-container') ?? [];
-
- // If we have unified resources, convert and reconstruct hierarchy
- if (dockerHostResources.length > 0) {
- return dockerHostResources.map(h => {
- const platformData = h.platformData as Record | undefined;
-
- // Find containers belonging to this host
- const hostContainers = dockerContainerResources
- .filter(c => c.parentId === h.id)
- .map(c => {
- const cPlatform = c.platformData as Record | undefined;
- return {
- id: c.id,
- name: c.name,
- image: cPlatform?.image as string ?? '',
- state: c.status === 'running' ? 'running' : 'exited',
- status: c.status,
- health: cPlatform?.health as string | undefined,
- cpuPercent: c.cpu?.current ?? 0,
- memoryUsageBytes: c.memory?.used ?? 0,
- memoryLimitBytes: c.memory?.total ?? 0,
- memoryPercent: c.memory?.current ?? 0,
- uptimeSeconds: c.uptime ?? 0,
- restartCount: cPlatform?.restartCount as number ?? 0,
- exitCode: cPlatform?.exitCode as number ?? 0,
- createdAt: cPlatform?.createdAt as number ?? 0,
- startedAt: cPlatform?.startedAt as number | undefined,
- finishedAt: cPlatform?.finishedAt as number | undefined,
- ports: cPlatform?.ports,
- labels: cPlatform?.labels as Record | undefined,
- networks: cPlatform?.networks,
- };
- });
-
- return {
- id: h.id,
- agentId: platformData?.agentId as string ?? h.id,
- hostname: h.identity?.hostname ?? h.name,
- displayName: h.displayName || h.name,
- customDisplayName: platformData?.customDisplayName as string | undefined,
- machineId: h.identity?.machineId,
- os: platformData?.os as string | undefined,
- kernelVersion: platformData?.kernelVersion as string | undefined,
- architecture: platformData?.architecture as string | undefined,
- runtime: platformData?.runtime as string ?? 'docker',
- runtimeVersion: platformData?.runtimeVersion as string | undefined,
- dockerVersion: platformData?.dockerVersion as string | undefined,
- cpus: platformData?.cpus as number ?? 0,
- totalMemoryBytes: h.memory?.total ?? 0,
- uptimeSeconds: h.uptime ?? 0,
- cpuUsagePercent: h.cpu?.current,
- loadAverage: platformData?.loadAverage as number[] | undefined,
- memory: h.memory ? {
- total: h.memory.total ?? 0,
- used: h.memory.used ?? 0,
- free: h.memory.free ?? 0,
- usage: h.memory.current,
- } : undefined,
- disks: platformData?.disks,
- networkInterfaces: platformData?.networkInterfaces,
- status: h.status === 'online' || h.status === 'running' ? 'online' : h.status,
- lastSeen: h.lastSeen,
- intervalSeconds: platformData?.intervalSeconds as number ?? 30,
- agentVersion: platformData?.agentVersion as string | undefined,
- containers: hostContainers,
- services: platformData?.services,
- tasks: platformData?.tasks,
- swarm: platformData?.swarm,
- tokenId: platformData?.tokenId as string | undefined,
- tokenName: platformData?.tokenName as string | undefined,
- tokenHint: platformData?.tokenHint as string | undefined,
- tokenLastUsedAt: platformData?.tokenLastUsedAt as number | undefined,
- hidden: platformData?.hidden as boolean | undefined,
- pendingUninstall: platformData?.pendingUninstall as boolean | undefined,
- command: platformData?.command,
- isLegacy: platformData?.isLegacy as boolean | undefined,
- };
- });
- }
-
- // Return empty array if no unified resources available
- return [];
- });
-
- return ;
+ return ;
}
+// Hosts route component - uses unified resources via useResourcesAsLegacy hook
function HostsRoute() {
const wsContext = useContext(WebSocketContext);
if (!wsContext) {
return Loading...
;
}
const { state } = wsContext;
-
- // Use unified resources if available, fall back to legacy state.hosts
- // During migration: resources may be empty initially, so we need the fallback
- const hosts = createMemo(() => {
- const unifiedHosts = state.resources?.filter(r => r.type === 'host') ?? [];
-
- // If we have unified resources, convert them to legacy Host format
- // (This will be simplified once HostsOverview is migrated to use resources directly)
- if (unifiedHosts.length > 0) {
- return unifiedHosts.map(r => {
- const platformData = r.platformData as Record | undefined;
- return {
- id: r.id,
- hostname: r.identity?.hostname ?? r.name,
- displayName: r.displayName || r.name,
- platform: platformData?.platform as string | undefined,
- osName: platformData?.osName as string | undefined,
- osVersion: platformData?.osVersion as string | undefined,
- kernelVersion: platformData?.kernelVersion as string | undefined,
- architecture: platformData?.architecture as string | undefined,
- cpuCount: platformData?.cpuCount as number | undefined,
- cpuUsage: r.cpu?.current,
- loadAverage: platformData?.loadAverage as number[] | undefined,
- memory: r.memory ? {
- total: r.memory.total ?? 0,
- used: r.memory.used ?? 0,
- free: r.memory.free ?? 0,
- usage: r.memory.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- disks: platformData?.disks as Array<{
- total: number;
- used: number;
- free: number;
- usage: number;
- mountpoint?: string;
- }> | undefined,
- diskIO: platformData?.diskIO,
- networkInterfaces: platformData?.networkInterfaces,
- sensors: platformData?.sensors,
- raid: platformData?.raid,
- status: r.status === 'online' || r.status === 'running' ? 'online' : r.status,
- uptimeSeconds: r.uptime,
- lastSeen: r.lastSeen,
- intervalSeconds: platformData?.intervalSeconds as number | undefined,
- agentVersion: platformData?.agentVersion as string | undefined,
- tokenId: platformData?.tokenId as string | undefined,
- tokenName: platformData?.tokenName as string | undefined,
- tags: r.tags,
- };
- });
- }
-
- // Return empty array if no unified resources available
- return [];
- });
+ const { asHosts } = useResourcesAsLegacy();
return (
-
+
);
}
@@ -870,142 +731,12 @@ function App() {
// Pass through the store directly (only when initialized)
const enhancedStore = () => wsStore();
- // Dashboard view using unified resources with fallback
+ // Dashboard view - uses unified resources via useResourcesAsLegacy hook
const DashboardView = () => {
- // Use unified resources if available, fall back to legacy data
- const vms = createMemo(() => {
- const vmResources = state().resources?.filter(r => r.type === 'vm') ?? [];
- if (vmResources.length > 0) {
- return vmResources.map(r => {
- const platformData = r.platformData as Record | undefined;
- return {
- id: r.id,
- vmid: platformData?.vmid as number ?? 0,
- name: r.name,
- node: platformData?.node as string ?? '',
- instance: platformData?.instance as string ?? r.platformId,
- status: r.status === 'running' ? 'running' : 'stopped',
- type: 'qemu',
- cpu: r.cpu?.current ?? 0,
- cpus: platformData?.cpus as number ?? 1,
- memory: r.memory ? {
- total: r.memory.total ?? 0,
- used: r.memory.used ?? 0,
- free: r.memory.free ?? 0,
- usage: r.memory.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- disk: r.disk ? {
- total: r.disk.total ?? 0,
- used: r.disk.used ?? 0,
- free: r.disk.free ?? 0,
- usage: r.disk.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- networkIn: r.network?.rxBytes ?? 0,
- networkOut: r.network?.txBytes ?? 0,
- diskRead: 0,
- diskWrite: 0,
- uptime: r.uptime ?? 0,
- template: platformData?.template as boolean ?? false,
- lastBackup: platformData?.lastBackup as number ?? 0,
- tags: r.tags ?? [],
- lock: platformData?.lock as string ?? '',
- lastSeen: new Date(r.lastSeen).toISOString(),
- };
- });
- }
- // Return empty array if no unified resources available
- return [];
- });
-
- const containers = createMemo(() => {
- const containerResources = state().resources?.filter(r => r.type === 'container') ?? [];
- if (containerResources.length > 0) {
- return containerResources.map(r => {
- const platformData = r.platformData as Record | undefined;
- return {
- id: r.id,
- vmid: platformData?.vmid as number ?? 0,
- name: r.name,
- node: platformData?.node as string ?? '',
- instance: platformData?.instance as string ?? r.platformId,
- status: r.status === 'running' ? 'running' : 'stopped',
- type: 'lxc',
- cpu: r.cpu?.current ?? 0,
- cpus: platformData?.cpus as number ?? 1,
- memory: r.memory ? {
- total: r.memory.total ?? 0,
- used: r.memory.used ?? 0,
- free: r.memory.free ?? 0,
- usage: r.memory.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- disk: r.disk ? {
- total: r.disk.total ?? 0,
- used: r.disk.used ?? 0,
- free: r.disk.free ?? 0,
- usage: r.disk.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- networkIn: r.network?.rxBytes ?? 0,
- networkOut: r.network?.txBytes ?? 0,
- diskRead: 0,
- diskWrite: 0,
- uptime: r.uptime ?? 0,
- template: platformData?.template as boolean ?? false,
- lastBackup: platformData?.lastBackup as number ?? 0,
- tags: r.tags ?? [],
- lock: platformData?.lock as string ?? '',
- lastSeen: new Date(r.lastSeen).toISOString(),
- };
- });
- }
- // Return empty array if no unified resources available
- return [];
- });
-
- const nodes = createMemo(() => {
- const nodeResources = state().resources?.filter(r => r.type === 'node') ?? [];
- if (nodeResources.length > 0) {
- return nodeResources.map(r => {
- const platformData = r.platformData as Record | undefined;
- return {
- id: r.id,
- name: r.name,
- displayName: r.displayName,
- instance: r.platformId,
- host: platformData?.host as string ?? '',
- status: r.status,
- type: 'node',
- cpu: r.cpu?.current ?? 0,
- memory: r.memory ? {
- total: r.memory.total ?? 0,
- used: r.memory.used ?? 0,
- free: r.memory.free ?? 0,
- usage: r.memory.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- disk: r.disk ? {
- total: r.disk.total ?? 0,
- used: r.disk.used ?? 0,
- free: r.disk.free ?? 0,
- usage: r.disk.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- uptime: r.uptime ?? 0,
- loadAverage: platformData?.loadAverage as number[] ?? [],
- kernelVersion: platformData?.kernelVersion as string ?? '',
- pveVersion: platformData?.pveVersion as string ?? '',
- cpuInfo: platformData?.cpuInfo ?? { model: '', cores: 0, sockets: 0, mhz: '' },
- temperature: platformData?.temperature,
- lastSeen: new Date(r.lastSeen).toISOString(),
- connectionHealth: platformData?.connectionHealth as string ?? 'unknown',
- isClusterMember: platformData?.isClusterMember as boolean | undefined,
- clusterName: platformData?.clusterName as string | undefined,
- };
- });
- }
- // Return empty array if no unified resources available
- return [];
- });
+ const { asVMs, asContainers, asNodes } = useResourcesAsLegacy();
return (
-
+
);
};
diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx
index 84a5a1a..3f56ecd 100644
--- a/frontend-modern/src/components/Dashboard/Dashboard.tsx
+++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx
@@ -723,6 +723,7 @@ export function Dashboard(props: DashboardProps) {
guests.forEach((guest) => {
// Node.ID is formatted as "instance-nodename", so we need to match that
const nodeId = `${guest.instance}-${guest.node}`;
+
if (!groups[nodeId]) {
groups[nodeId] = [];
}
diff --git a/frontend-modern/src/hooks/useResources.ts b/frontend-modern/src/hooks/useResources.ts
index 5473d90..4ca7c46 100644
--- a/frontend-modern/src/hooks/useResources.ts
+++ b/frontend-modern/src/hooks/useResources.ts
@@ -231,76 +231,82 @@ export function useResourcesAsLegacy() {
// Convert resources to legacy VM format
const asVMs = createMemo(() => {
- return byType('vm').map(r => ({
- id: r.id,
- vmid: parseInt(r.id.split('-').pop() ?? '0', 10),
- name: r.name,
- node: r.parentId ?? '',
- instance: r.platformId,
- status: r.status === 'running' ? 'running' : 'stopped',
- type: 'qemu',
- cpu: r.cpu?.current ?? 0,
- cpus: 1, // Not available in unified model
- memory: r.memory ? {
- total: r.memory.total ?? 0,
- used: r.memory.used ?? 0,
- free: r.memory.free ?? 0,
- usage: r.memory.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- disk: r.disk ? {
- total: r.disk.total ?? 0,
- used: r.disk.used ?? 0,
- free: r.disk.free ?? 0,
- usage: r.disk.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- networkIn: r.network?.rxBytes ?? 0,
- networkOut: r.network?.txBytes ?? 0,
- diskRead: 0,
- diskWrite: 0,
- uptime: r.uptime ?? 0,
- template: false,
- lastBackup: 0,
- tags: r.tags ?? [],
- lock: '',
- lastSeen: new Date(r.lastSeen).toISOString(),
- }));
+ return byType('vm').map(r => {
+ const platformData = r.platformData as Record | undefined;
+ return {
+ id: r.id,
+ vmid: platformData?.vmid as number ?? parseInt(r.id.split('-').pop() ?? '0', 10),
+ name: r.name,
+ node: platformData?.node as string ?? '',
+ instance: platformData?.instance as string ?? r.platformId,
+ status: r.status === 'running' ? 'running' : 'stopped',
+ type: 'qemu',
+ cpu: (r.cpu?.current ?? 0) / 100, // Convert from percentage to ratio for Dashboard
+ cpus: platformData?.cpus as number ?? 1,
+ memory: r.memory ? {
+ total: r.memory.total ?? 0,
+ used: r.memory.used ?? 0,
+ free: r.memory.free ?? 0,
+ usage: r.memory.current,
+ } : { total: 0, used: 0, free: 0, usage: 0 },
+ disk: r.disk ? {
+ total: r.disk.total ?? 0,
+ used: r.disk.used ?? 0,
+ free: r.disk.free ?? 0,
+ usage: r.disk.current,
+ } : { total: 0, used: 0, free: 0, usage: 0 },
+ networkIn: r.network?.rxBytes ?? 0,
+ networkOut: r.network?.txBytes ?? 0,
+ diskRead: platformData?.diskRead as number ?? 0,
+ diskWrite: platformData?.diskWrite as number ?? 0,
+ uptime: r.uptime ?? 0,
+ template: platformData?.template as boolean ?? false,
+ lastBackup: platformData?.lastBackup as number ?? 0,
+ tags: r.tags ?? [],
+ lock: platformData?.lock as string ?? '',
+ lastSeen: new Date(r.lastSeen).toISOString(),
+ };
+ });
});
// Convert resources to legacy Container format
const asContainers = createMemo(() => {
- return byType('container').map(r => ({
- id: r.id,
- vmid: parseInt(r.id.split('-').pop() ?? '0', 10),
- name: r.name,
- node: r.parentId ?? '',
- instance: r.platformId,
- status: r.status === 'running' ? 'running' : 'stopped',
- type: 'lxc',
- cpu: r.cpu?.current ?? 0,
- cpus: 1,
- memory: r.memory ? {
- total: r.memory.total ?? 0,
- used: r.memory.used ?? 0,
- free: r.memory.free ?? 0,
- usage: r.memory.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- disk: r.disk ? {
- total: r.disk.total ?? 0,
- used: r.disk.used ?? 0,
- free: r.disk.free ?? 0,
- usage: r.disk.current,
- } : { total: 0, used: 0, free: 0, usage: 0 },
- networkIn: r.network?.rxBytes ?? 0,
- networkOut: r.network?.txBytes ?? 0,
- diskRead: 0,
- diskWrite: 0,
- uptime: r.uptime ?? 0,
- template: false,
- lastBackup: 0,
- tags: r.tags ?? [],
- lock: '',
- lastSeen: new Date(r.lastSeen).toISOString(),
- }));
+ return byType('container').map(r => {
+ const platformData = r.platformData as Record | undefined;
+ return {
+ id: r.id,
+ vmid: platformData?.vmid as number ?? parseInt(r.id.split('-').pop() ?? '0', 10),
+ name: r.name,
+ node: platformData?.node as string ?? '',
+ instance: platformData?.instance as string ?? r.platformId,
+ status: r.status === 'running' ? 'running' : 'stopped',
+ type: 'lxc',
+ cpu: (r.cpu?.current ?? 0) / 100, // Convert from percentage to ratio for Dashboard
+ cpus: platformData?.cpus as number ?? 1,
+ memory: r.memory ? {
+ total: r.memory.total ?? 0,
+ used: r.memory.used ?? 0,
+ free: r.memory.free ?? 0,
+ usage: r.memory.current,
+ } : { total: 0, used: 0, free: 0, usage: 0 },
+ disk: r.disk ? {
+ total: r.disk.total ?? 0,
+ used: r.disk.used ?? 0,
+ free: r.disk.free ?? 0,
+ usage: r.disk.current,
+ } : { total: 0, used: 0, free: 0, usage: 0 },
+ networkIn: r.network?.rxBytes ?? 0,
+ networkOut: r.network?.txBytes ?? 0,
+ diskRead: platformData?.diskRead as number ?? 0,
+ diskWrite: platformData?.diskWrite as number ?? 0,
+ uptime: r.uptime ?? 0,
+ template: platformData?.template as boolean ?? false,
+ lastBackup: platformData?.lastBackup as number ?? 0,
+ tags: r.tags ?? [],
+ lock: platformData?.lock as string ?? '',
+ lastSeen: new Date(r.lastSeen).toISOString(),
+ };
+ });
});
// Convert resources to legacy Host format
@@ -377,11 +383,134 @@ export function useResourcesAsLegacy() {
});
});
+ // Convert resources to legacy Node format
+ const asNodes = createMemo(() => {
+ return byType('node').map(r => {
+ const platformData = r.platformData as Record | undefined;
+ return {
+ id: r.id,
+ name: r.name,
+ displayName: r.displayName,
+ instance: r.platformId,
+ host: platformData?.host as string ?? '',
+ status: r.status,
+ type: 'node',
+ cpu: r.cpu?.current ?? 0,
+ memory: r.memory ? {
+ total: r.memory.total ?? 0,
+ used: r.memory.used ?? 0,
+ free: r.memory.free ?? 0,
+ usage: r.memory.current,
+ } : { total: 0, used: 0, free: 0, usage: 0 },
+ disk: r.disk ? {
+ total: r.disk.total ?? 0,
+ used: r.disk.used ?? 0,
+ free: r.disk.free ?? 0,
+ usage: r.disk.current,
+ } : { total: 0, used: 0, free: 0, usage: 0 },
+ uptime: r.uptime ?? 0,
+ loadAverage: platformData?.loadAverage as number[] ?? [],
+ kernelVersion: platformData?.kernelVersion as string ?? '',
+ pveVersion: platformData?.pveVersion as string ?? '',
+ cpuInfo: platformData?.cpuInfo ?? { model: '', cores: 0, sockets: 0, mhz: '' },
+ temperature: platformData?.temperature,
+ lastSeen: new Date(r.lastSeen).toISOString(),
+ connectionHealth: platformData?.connectionHealth as string ?? 'unknown',
+ isClusterMember: platformData?.isClusterMember as boolean | undefined,
+ clusterName: platformData?.clusterName as string | undefined,
+ };
+ });
+ });
+
+ // Convert resources to legacy DockerHost format (including nested containers)
+ const asDockerHosts = createMemo(() => {
+ const dockerHostResources = byType('docker-host');
+ const dockerContainerResources = byType('docker-container');
+
+ return dockerHostResources.map(h => {
+ const platformData = h.platformData as Record | undefined;
+
+ // Find containers belonging to this host
+ const hostContainers = dockerContainerResources
+ .filter(c => c.parentId === h.id)
+ .map(c => {
+ const cPlatform = c.platformData as Record | undefined;
+ return {
+ id: c.id,
+ name: c.name,
+ image: cPlatform?.image as string ?? '',
+ state: c.status === 'running' ? 'running' : 'exited',
+ status: c.status,
+ health: cPlatform?.health as string | undefined,
+ cpuPercent: c.cpu?.current ?? 0,
+ memoryUsageBytes: c.memory?.used ?? 0,
+ memoryLimitBytes: c.memory?.total ?? 0,
+ memoryPercent: c.memory?.current ?? 0,
+ uptimeSeconds: c.uptime ?? 0,
+ restartCount: cPlatform?.restartCount as number ?? 0,
+ exitCode: cPlatform?.exitCode as number ?? 0,
+ createdAt: cPlatform?.createdAt as number ?? 0,
+ startedAt: cPlatform?.startedAt as number | undefined,
+ finishedAt: cPlatform?.finishedAt as number | undefined,
+ ports: cPlatform?.ports,
+ labels: cPlatform?.labels as Record | undefined,
+ networks: cPlatform?.networks,
+ };
+ });
+
+ return {
+ id: h.id,
+ agentId: platformData?.agentId as string ?? h.id,
+ hostname: h.identity?.hostname ?? h.name,
+ displayName: h.displayName || h.name,
+ customDisplayName: platformData?.customDisplayName as string | undefined,
+ machineId: h.identity?.machineId,
+ os: platformData?.os as string | undefined,
+ kernelVersion: platformData?.kernelVersion as string | undefined,
+ architecture: platformData?.architecture as string | undefined,
+ runtime: platformData?.runtime as string ?? 'docker',
+ runtimeVersion: platformData?.runtimeVersion as string | undefined,
+ dockerVersion: platformData?.dockerVersion as string | undefined,
+ cpus: platformData?.cpus as number ?? 0,
+ totalMemoryBytes: h.memory?.total ?? 0,
+ uptimeSeconds: h.uptime ?? 0,
+ cpuUsagePercent: h.cpu?.current,
+ loadAverage: platformData?.loadAverage as number[] | undefined,
+ memory: h.memory ? {
+ total: h.memory.total ?? 0,
+ used: h.memory.used ?? 0,
+ free: h.memory.free ?? 0,
+ usage: h.memory.current,
+ } : undefined,
+ disks: platformData?.disks,
+ networkInterfaces: platformData?.networkInterfaces,
+ status: h.status === 'online' || h.status === 'running' ? 'online' : h.status,
+ lastSeen: h.lastSeen,
+ intervalSeconds: platformData?.intervalSeconds as number ?? 30,
+ agentVersion: platformData?.agentVersion as string | undefined,
+ containers: hostContainers,
+ services: platformData?.services,
+ tasks: platformData?.tasks,
+ swarm: platformData?.swarm,
+ tokenId: platformData?.tokenId as string | undefined,
+ tokenName: platformData?.tokenName as string | undefined,
+ tokenHint: platformData?.tokenHint as string | undefined,
+ tokenLastUsedAt: platformData?.tokenLastUsedAt as number | undefined,
+ hidden: platformData?.hidden as boolean | undefined,
+ pendingUninstall: platformData?.pendingUninstall as boolean | undefined,
+ command: platformData?.command,
+ isLegacy: platformData?.isLegacy as boolean | undefined,
+ };
+ });
+ });
+
return {
resources,
asVMs,
asContainers,
asHosts,
+ asNodes,
+ asDockerHosts,
};
}
diff --git a/frontend-modern/src/stores/websocket.ts b/frontend-modern/src/stores/websocket.ts
index e1fd52a..bfe9aa8 100644
--- a/frontend-modern/src/stores/websocket.ts
+++ b/frontend-modern/src/stores/websocket.ts
@@ -584,6 +584,10 @@ export function createWebSocketStore(url: string) {
setState('physicalDisks', reconcile(message.data.physicalDisks, { key: 'id' }));
// Handle unified resources
if (message.data.resources !== undefined) {
+ logger.debug('[WebSocket] Updating resources', {
+ count: message.data.resources?.length || 0,
+ types: [...new Set(message.data.resources?.map((r: any) => r.type) || [])],
+ });
setState('resources', reconcile(message.data.resources, { key: 'id' }));
}
// Sync active alerts from state
diff --git a/internal/resources/converters.go b/internal/resources/converters.go
index f5218c3..254f088 100644
--- a/internal/resources/converters.go
+++ b/internal/resources/converters.go
@@ -154,8 +154,8 @@ func FromVM(vm models.VM) Resource {
}
platformDataJSON, _ := json.Marshal(platformData)
- // Parent is the node
- parentID := fmt.Sprintf("%s/node/%s", vm.Instance, vm.Node)
+ // Parent is the node - format matches Node.ID: instance-nodename
+ parentID := fmt.Sprintf("%s-%s", vm.Instance, vm.Node)
return Resource{
ID: vm.ID,
@@ -232,8 +232,8 @@ func FromContainer(ct models.Container) Resource {
}
platformDataJSON, _ := json.Marshal(platformData)
- // Parent is the node
- parentID := fmt.Sprintf("%s/node/%s", ct.Instance, ct.Node)
+ // Parent is the node - format matches Node.ID: instance-nodename
+ parentID := fmt.Sprintf("%s-%s", ct.Instance, ct.Node)
return Resource{
ID: ct.ID,
@@ -758,7 +758,7 @@ func FromStorage(s models.Storage) Resource {
PlatformID: s.Instance,
PlatformType: PlatformProxmoxPVE,
SourceType: SourceAPI,
- ParentID: fmt.Sprintf("%s/node/%s", s.Instance, s.Node),
+ ParentID: fmt.Sprintf("%s-%s", s.Instance, s.Node), // Format matches Node.ID: instance-nodename
Status: status,
Disk: disk,
LastSeen: time.Now(), // Storage doesn't have LastSeen
diff --git a/internal/resources/converters_test.go b/internal/resources/converters_test.go
index e4c4a57..8935252 100644
--- a/internal/resources/converters_test.go
+++ b/internal/resources/converters_test.go
@@ -160,8 +160,8 @@ func TestFromVM(t *testing.T) {
if r.Status != StatusRunning {
t.Errorf("Expected status %s, got %s", StatusRunning, r.Status)
}
- if r.ParentID != "pve1/node/node1" {
- t.Errorf("Expected parent pve1/node/node1, got %s", r.ParentID)
+ if r.ParentID != "pve1-node1" {
+ t.Errorf("Expected parent pve1-node1, got %s", r.ParentID)
}
// Check CPU is converted to percentage
@@ -229,8 +229,8 @@ func TestFromContainer(t *testing.T) {
if r.Status != StatusStopped {
t.Errorf("Expected status %s, got %s", StatusStopped, r.Status)
}
- if r.ParentID != "pve1/node/node1" {
- t.Errorf("Expected parent pve1/node/node1, got %s", r.ParentID)
+ if r.ParentID != "pve1-node1" {
+ t.Errorf("Expected parent pve1-node1, got %s", r.ParentID)
}
var pd ContainerPlatformData