diff --git a/frontend-modern/src/hooks/useResources.ts b/frontend-modern/src/hooks/useResources.ts
new file mode 100644
index 0000000..0f77855
--- /dev/null
+++ b/frontend-modern/src/hooks/useResources.ts
@@ -0,0 +1,315 @@
+/**
+ * useResources Hook
+ *
+ * Provides reactive access to unified resources from the WebSocket state.
+ * This is the primary way for frontend components to access resource data
+ * as we migrate away from legacy arrays (nodes, vms, containers, etc.).
+ *
+ * Example usage:
+ * ```tsx
+ * const { resources, infra, workloads, filtered } = useResources();
+ *
+ * // Get all resources
+ * {r => {r.name}
}
+ *
+ * // Get only infrastructure (nodes, hosts)
+ * {r => {r.name}
}
+ *
+ * // Get filtered workloads
+ * const vms = filtered({ types: ['vm'], statuses: ['running'] });
+ * {r => {r.name}
}
+ * ```
+ */
+
+import { createMemo, type Accessor } from 'solid-js';
+import { getGlobalWebSocketStore } from '@/stores/websocket-global';
+import type {
+ Resource,
+ ResourceType,
+ PlatformType,
+ ResourceStatus,
+ ResourceFilter,
+} from '@/types/resource';
+import {
+ isInfrastructure,
+ isWorkload,
+ getDisplayName,
+ getCpuPercent,
+ getMemoryPercent,
+ getDiskPercent,
+} from '@/types/resource';
+
+export interface UseResourcesReturn {
+ /** All unified resources */
+ resources: Accessor;
+
+ /** Infrastructure resources only (nodes, hosts, docker-hosts) */
+ infra: Accessor;
+
+ /** Workload resources only (vms, containers, docker-containers) */
+ workloads: Accessor;
+
+ /** Resources by type */
+ byType: (type: ResourceType) => Resource[];
+
+ /** Resources by platform */
+ byPlatform: (platform: PlatformType) => Resource[];
+
+ /** Filter resources with multiple criteria */
+ filtered: (filter: ResourceFilter) => Resource[];
+
+ /** Get a single resource by ID */
+ get: (id: string) => Resource | undefined;
+
+ /** Get children of a resource (e.g., VMs of a node) */
+ children: (parentId: string) => Resource[];
+
+ /** Count of resources by status */
+ statusCounts: Accessor>;
+
+ /** Top consumers by CPU */
+ topByCpu: (limit?: number) => Resource[];
+
+ /** Top consumers by memory */
+ topByMemory: (limit?: number) => Resource[];
+
+ /** Whether resources are available */
+ hasResources: Accessor;
+}
+
+/**
+ * Hook for accessing unified resources with reactive filtering.
+ */
+export function useResources(): UseResourcesReturn {
+ // Get the WebSocket store instance
+ const wsStore = getGlobalWebSocketStore();
+
+ // All resources from WebSocket state
+ const resources = createMemo(() => {
+ return wsStore.state.resources ?? [];
+ });
+
+ // Pre-filtered memos for common use cases
+ const infra = createMemo(() => {
+ return resources().filter(isInfrastructure);
+ });
+
+ const workloads = createMemo(() => {
+ return resources().filter(isWorkload);
+ });
+
+ const hasResources = createMemo(() => {
+ return resources().length > 0;
+ });
+
+ // Status counts
+ const statusCounts = createMemo>(() => {
+ const counts: Record = {
+ online: 0,
+ offline: 0,
+ running: 0,
+ stopped: 0,
+ degraded: 0,
+ paused: 0,
+ unknown: 0,
+ };
+
+ for (const r of resources()) {
+ if (r.status in counts) {
+ counts[r.status as ResourceStatus]++;
+ }
+ }
+
+ return counts;
+ });
+
+ // Filter by type
+ const byType = (type: ResourceType): Resource[] => {
+ return resources().filter(r => r.type === type);
+ };
+
+ // Filter by platform
+ const byPlatform = (platform: PlatformType): Resource[] => {
+ return resources().filter(r => r.platformType === platform);
+ };
+
+ // Complex filtering
+ const filtered = (filter: ResourceFilter): Resource[] => {
+ let result = resources();
+
+ // Filter by types
+ if (filter.types && filter.types.length > 0) {
+ result = result.filter(r => filter.types!.includes(r.type));
+ }
+
+ // Filter by platforms
+ if (filter.platforms && filter.platforms.length > 0) {
+ result = result.filter(r => filter.platforms!.includes(r.platformType));
+ }
+
+ // Filter by statuses
+ if (filter.statuses && filter.statuses.length > 0) {
+ result = result.filter(r => filter.statuses!.includes(r.status));
+ }
+
+ // Filter by parent
+ if (filter.parentId) {
+ result = result.filter(r => r.parentId === filter.parentId);
+ }
+
+ // Filter by cluster
+ if (filter.clusterId) {
+ result = result.filter(r => r.clusterId === filter.clusterId);
+ }
+
+ // Filter by alerts
+ if (filter.hasAlerts) {
+ result = result.filter(r => r.alerts && r.alerts.length > 0);
+ }
+
+ // Search filter (name, displayName)
+ if (filter.search && filter.search.trim()) {
+ const term = filter.search.toLowerCase().trim();
+ result = result.filter(r => {
+ const name = getDisplayName(r).toLowerCase();
+ const id = r.id.toLowerCase();
+ return name.includes(term) || id.includes(term);
+ });
+ }
+
+ return result;
+ };
+
+ // Get single resource by ID
+ const get = (id: string): Resource | undefined => {
+ return resources().find(r => r.id === id);
+ };
+
+ // Get children of a parent
+ const children = (parentId: string): Resource[] => {
+ return resources().filter(r => r.parentId === parentId);
+ };
+
+ // Top by CPU
+ const topByCpu = (limit: number = 10): Resource[] => {
+ return [...resources()]
+ .filter(r => r.cpu && r.cpu.current > 0)
+ .sort((a, b) => getCpuPercent(b) - getCpuPercent(a))
+ .slice(0, limit);
+ };
+
+ // Top by memory
+ const topByMemory = (limit: number = 10): Resource[] => {
+ return [...resources()]
+ .filter(r => r.memory)
+ .sort((a, b) => getMemoryPercent(b) - getMemoryPercent(a))
+ .slice(0, limit);
+ };
+
+ return {
+ resources,
+ infra,
+ workloads,
+ byType,
+ byPlatform,
+ filtered,
+ get,
+ children,
+ statusCounts,
+ topByCpu,
+ topByMemory,
+ hasResources,
+ };
+}
+
+/**
+ * Helper hook for resource-to-legacy-type conversion.
+ * This helps during migration when components still expect legacy types.
+ */
+export function useResourcesAsLegacy() {
+ const { resources, byType } = useResources();
+
+ // 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(),
+ }));
+ });
+
+ // 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 {
+ resources,
+ asVMs,
+ asContainers,
+ };
+}
+
+// Re-export types and helpers
+export type { Resource, ResourceType, PlatformType, ResourceStatus, ResourceFilter };
+export { isInfrastructure, isWorkload, getDisplayName, getCpuPercent, getMemoryPercent, getDiskPercent };
diff --git a/frontend-modern/src/types/api.ts b/frontend-modern/src/types/api.ts
index 5f10f10..bb8685b 100644
--- a/frontend-modern/src/types/api.ts
+++ b/frontend-modern/src/types/api.ts
@@ -1,5 +1,7 @@
// Properly typed TypeScript interfaces for Pulse API
+import type { Resource } from './resource';
+
export interface State {
nodes: Node[];
vms: VM[];
@@ -25,6 +27,8 @@ export interface State {
recentlyResolved: ResolvedAlert[];
lastUpdate: string;
temperatureMonitoringEnabled?: boolean;
+ // Unified resources (new data model - eventually replaces legacy arrays above)
+ resources?: Resource[];
}
export interface RemovedDockerHost {
diff --git a/frontend-modern/src/types/resource.ts b/frontend-modern/src/types/resource.ts
new file mode 100644
index 0000000..59dbf8a
--- /dev/null
+++ b/frontend-modern/src/types/resource.ts
@@ -0,0 +1,194 @@
+/**
+ * Unified Resource Types
+ *
+ * These types define the unified resource model that normalizes all monitored
+ * entities (VMs, containers, hosts, etc.) into a common structure.
+ *
+ * The frontend receives these via WebSocket state.resources[].
+ */
+
+// Resource types - what kind of entity is being monitored
+export type ResourceType =
+ | 'node' // Proxmox VE node
+ | 'host' // Standalone host (via host-agent)
+ | 'docker-host' // Docker/Podman host
+ | 'k8s-node' // Kubernetes node
+ | 'truenas' // TrueNAS system
+ | 'vm' // Proxmox VM
+ | 'container' // LXC container
+ | 'docker-container' // Docker container
+ | 'pod' // Kubernetes pod
+ | 'jail' // BSD jail
+ | 'docker-service' // Docker Swarm service
+ | 'k8s-deployment' // Kubernetes deployment
+ | 'k8s-service' // Kubernetes service
+ | 'storage' // Storage resource
+ | 'datastore' // PBS datastore
+ | 'pool' // ZFS/Ceph pool
+ | 'dataset' // ZFS dataset
+ | 'pbs' // Proxmox Backup Server
+ | 'pmg'; // Proxmox Mail Gateway
+
+// Platform types - which system the resource comes from
+export type PlatformType =
+ | 'proxmox-pve'
+ | 'proxmox-pbs'
+ | 'proxmox-pmg'
+ | 'docker'
+ | 'kubernetes'
+ | 'truenas'
+ | 'host-agent';
+
+// Source types - how data is collected
+export type SourceType =
+ | 'api' // Data from polling an API
+ | 'agent' // Data pushed from agent
+ | 'hybrid'; // Both sources, agent preferred
+
+// Resource status - operational state
+export type ResourceStatus =
+ | 'online'
+ | 'offline'
+ | 'running'
+ | 'stopped'
+ | 'degraded'
+ | 'paused'
+ | 'unknown';
+
+// Metric value with optional limits
+export interface ResourceMetric {
+ current: number; // Current value (percentage or bytes)
+ total?: number; // Total capacity (bytes) - null for percentages
+ used?: number; // Used amount (bytes)
+ free?: number; // Free amount (bytes)
+}
+
+// Network I/O metrics
+export interface ResourceNetwork {
+ rxBytes: number; // Total bytes received
+ txBytes: number; // Total bytes transmitted
+}
+
+// Alert associated with a resource
+export interface ResourceAlert {
+ id: string;
+ type: string; // cpu, memory, disk, temperature, etc.
+ level: string; // warning, critical
+ message: string;
+ value: number;
+ threshold: number;
+ startTime: number; // Unix milliseconds
+}
+
+// Identity information for deduplication
+export interface ResourceIdentity {
+ hostname?: string;
+ machineId?: string;
+ ips?: string[];
+}
+
+/**
+ * The core unified Resource type.
+ * This is what the frontend receives from WebSocket state.resources[].
+ */
+export interface Resource {
+ // Identity
+ id: string;
+ type: ResourceType;
+ name: string;
+ displayName: string;
+
+ // Platform/Source
+ platformId: string;
+ platformType: PlatformType;
+ sourceType: SourceType;
+
+ // Hierarchy
+ parentId?: string; // Parent resource (e.g., VM -> Node)
+ clusterId?: string; // Cluster membership
+
+ // Universal Metrics
+ status: ResourceStatus;
+ cpu?: ResourceMetric;
+ memory?: ResourceMetric;
+ disk?: ResourceMetric;
+ network?: ResourceNetwork;
+ temperature?: number;
+ uptime?: number; // Seconds
+
+ // Metadata
+ tags?: string[];
+ labels?: Record;
+ lastSeen: number; // Unix milliseconds
+ alerts?: ResourceAlert[];
+
+ // Identity for deduplication
+ identity?: ResourceIdentity;
+
+ // Platform-specific data (varies by type)
+ platformData?: Record;
+}
+
+/**
+ * Helper type guards
+ */
+export function isInfrastructure(r: Resource): boolean {
+ return ['node', 'host', 'docker-host', 'k8s-node', 'truenas'].includes(r.type);
+}
+
+export function isWorkload(r: Resource): boolean {
+ return ['vm', 'container', 'docker-container', 'pod', 'jail'].includes(r.type);
+}
+
+export function isStorage(r: Resource): boolean {
+ return ['storage', 'datastore', 'pool', 'dataset'].includes(r.type);
+}
+
+/**
+ * Resource filtering options
+ */
+export interface ResourceFilter {
+ types?: ResourceType[];
+ platforms?: PlatformType[];
+ statuses?: ResourceStatus[];
+ parentId?: string;
+ clusterId?: string;
+ hasAlerts?: boolean;
+ search?: string;
+}
+
+/**
+ * Helper to get effective display name
+ */
+export function getDisplayName(r: Resource): string {
+ return r.displayName || r.name;
+}
+
+/**
+ * Helper to get CPU percentage
+ */
+export function getCpuPercent(r: Resource): number {
+ return r.cpu?.current ?? 0;
+}
+
+/**
+ * Helper to get memory percentage
+ */
+export function getMemoryPercent(r: Resource): number {
+ if (!r.memory) return 0;
+ if (r.memory.total && r.memory.used) {
+ return (r.memory.used / r.memory.total) * 100;
+ }
+ return r.memory.current;
+}
+
+/**
+ * Helper to get disk percentage
+ */
+export function getDiskPercent(r: Resource): number {
+ if (!r.disk) return 0;
+ if (r.disk.total && r.disk.used) {
+ return (r.disk.used / r.disk.total) * 100;
+ }
+ return r.disk.current;
+}