From 7e8611e0aef2c6f46ce4f4c9a093c672e36e537e Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 8 Dec 2025 09:16:26 +0000 Subject: [PATCH] cleanup: consolidate unified resources adapters into useResourcesAsLegacy hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This cleanup addresses transition debt from the unified resources migration: Frontend cleanup: - Move all Resource→Legacy type conversions to useResourcesAsLegacy() hook - Add asNodes() and asDockerHosts() adapter functions to the hook - Simplify DockerRoute, HostsRoute, DashboardView to use the centralized hook - Remove ~300 lines of duplicated adapter code from App.tsx - Remove debug console.log statements from Dashboard.tsx - Fix CPU value conversion (divide by 100) for Dashboard compatibility Backend fixes (from previous session): - Fix parentID format in converters (VM, Container, Storage) to match Node.ID - Format changed from 'instance/node/nodename' to 'instance-nodename' - Update tests to match new parentID format This consolidates all legacy type conversion logic in one place, making future cleanup easier when components are migrated to use unified resources directly. --- .gemini/docs/unified-resource-architecture.md | 2 +- frontend-modern/src/App.tsx | 291 +----------------- .../src/components/Dashboard/Dashboard.tsx | 1 + frontend-modern/src/hooks/useResources.ts | 261 ++++++++++++---- frontend-modern/src/stores/websocket.ts | 4 + internal/resources/converters.go | 10 +- internal/resources/converters_test.go | 8 +- 7 files changed, 221 insertions(+), 356 deletions(-) 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