import type { Disk, Memory, Node, PBSBackupJob, PBSGarbageJob, PBSInstance, PBSNamespace, PBSPruneJob, PBSSyncJob, PBSDatastore, PBSVerifyJob, PMGDomainStat, PMGInstance, PMGMailCountPoint, PMGMailStats, PMGNodeStatus, PMGQuarantineTotals, PMGRelayDomain, PMGSpamBucket, Temperature, } from '@/types/api'; import type { Resource } from '@/types/resource'; import { getActionableAgentIdFromResource, getExplicitResourceClusterName, } from '@/utils/agentResources'; import { getPreferredInfrastructureDisplayName, getPreferredResourceClusterName, getPreferredResourceHostname, } from '@/utils/resourceIdentity'; import { normalizeSourcePlatformKey, resolvePlatformTypeFromSources, resolveSourceTypeFromSources, } from '@/utils/sourcePlatforms'; type JsonRecord = Record; const asRecord = (value: unknown): JsonRecord | undefined => value && typeof value === 'object' ? (value as JsonRecord) : undefined; const asString = (value: unknown): string | undefined => typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; const asNumber = (value: unknown): number | undefined => typeof value === 'number' && Number.isFinite(value) ? value : undefined; const asBoolean = (value: unknown): boolean | undefined => typeof value === 'boolean' ? value : undefined; const asArray = (value: unknown): unknown[] => (Array.isArray(value) ? value : []); const toISOTime = (value: unknown, fallbackMs?: number): string => { const asStr = asString(value); if (asStr) return asStr; if (typeof fallbackMs === 'number' && Number.isFinite(fallbackMs)) { return new Date(fallbackMs).toISOString(); } return new Date(0).toISOString(); }; const getCanonicalPlatformId = (resource: Resource): string | undefined => { const platformId = resource.canonicalIdentity?.platformId; return typeof platformId === 'string' && platformId.trim().length > 0 ? platformId.trim() : undefined; }; const normalizeResourceIdentityToken = (value: string | undefined): string | undefined => { if (!value) return undefined; const normalized = value.trim().toLowerCase(); return normalized.length > 0 ? normalized : undefined; }; export const resourcePlatformData = (resource: Resource): Record | undefined => asRecord(resource.platformData); const mergeStringArrays = (incoming?: string[], existing?: string[]): string[] | undefined => { const merged = [...(incoming ?? []), ...(existing ?? [])] .map((value) => asString(value)) .filter((value): value is string => Boolean(value)); return merged.length > 0 ? Array.from(new Set(merged)) : undefined; }; const readStringArray = (value: unknown): string[] | undefined => { if (!Array.isArray(value)) return undefined; const normalized = value .map((entry) => asString(entry)) .filter((entry): entry is string => Boolean(entry)); return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined; }; const normalizeSourceToken = (value: string): string => normalizeSourcePlatformKey(value) || value.trim().toLowerCase(); const sourceListHas = (sources: string[] | undefined, ...candidates: string[]): boolean => { if (!sources || sources.length === 0) return false; const sourceSet = new Set(sources.map((source) => normalizeSourceToken(source))); return candidates.some((candidate) => sourceSet.has(normalizeSourceToken(candidate))); }; const shouldKeepSourceFacet = ( authoritativeSources: string[] | undefined, ...sourceCandidates: string[] ): boolean => !authoritativeSources || sourceListHas(authoritativeSources, ...sourceCandidates); const mergeRecord = (incoming?: T, existing?: T): T | undefined => { if (!incoming) return existing; if (!existing) return incoming; return { ...existing, ...incoming }; }; const mergePlatformData = ( incomingValue: Resource['platformData'], existingValue: Resource['platformData'], ): Resource['platformData'] => { const incoming = asRecord(incomingValue); const existing = asRecord(existingValue); if (!incoming) return existingValue; if (!existing) return incomingValue; const incomingSources = readStringArray(incoming.sources); const existingSources = readStringArray(existing.sources); const sources = incomingSources ?? existingSources; const merged: JsonRecord = { ...existing, ...incoming }; for (const [key, ...sourceCandidates] of [ ['agent', 'agent'], ['docker', 'docker'], ['proxmox', 'proxmox-pve'], ['pbs', 'proxmox-pbs'], ['pmg', 'proxmox-pmg'], ['kubernetes', 'kubernetes'], ['vmware', 'vmware-vsphere'], ['availability', 'availability'], ] as const) { if (!shouldKeepSourceFacet(incomingSources, ...sourceCandidates)) { delete merged[key]; continue; } const nested = mergeRecord(asRecord(incoming[key]), asRecord(existing[key])); if (nested) { merged[key] = nested; } } for (const key of ['storage', 'physicalDisk', 'ceph', 'metrics', 'discoveryTarget']) { const nested = mergeRecord(asRecord(incoming[key]), asRecord(existing[key])); if (nested) { merged[key] = nested; } } const sourceStatus = mergeRecord( asRecord(incoming.sourceStatus), asRecord(existing.sourceStatus), ); if (sourceStatus) { merged.sourceStatus = sourceStatus; } if (sources) { merged.sources = sources; } return merged; }; const getResourceRecord = (resource: Resource): JsonRecord => resource as unknown as JsonRecord; const getExplicitResourceSources = (resource: Resource): string[] | undefined => readStringArray(getResourceRecord(resource).sources); const getFacetRecord = ( resource: Resource, platformData: Resource['platformData'] | undefined, key: string, ): JsonRecord | undefined => { const resourceRecord = getResourceRecord(resource); const platformRecord = asRecord(platformData); return asRecord(resourceRecord[key]) || asRecord(platformRecord?.[key]); }; const deriveLegacySourceList = ( resource: Resource, platformData: Resource['platformData'] | undefined = resource.platformData, ): string[] | undefined => { const resourceSources = getExplicitResourceSources(resource); if (resourceSources && resourceSources.length > 0) { return resourceSources; } const sources: string[] = []; if (getFacetRecord(resource, platformData, 'proxmox')) sources.push('proxmox'); if (getFacetRecord(resource, platformData, 'pbs')) sources.push('pbs'); if (getFacetRecord(resource, platformData, 'pmg')) sources.push('pmg'); if (getFacetRecord(resource, platformData, 'vmware')) sources.push('vmware'); if (getFacetRecord(resource, platformData, 'kubernetes')) sources.push('kubernetes'); if (getFacetRecord(resource, platformData, 'docker')) sources.push('docker'); if (getFacetRecord(resource, platformData, 'availability')) sources.push('availability'); if (getFacetRecord(resource, platformData, 'agent')) sources.push('agent'); if (sources.length > 0) { return Array.from(new Set(sources)); } if ( resource.type === 'network-endpoint' || resource.platformType === 'availability' || Boolean(resource.availability) || Boolean(asRecord(resource.platformData)?.availability) ) { return ['availability']; } switch (resource.platformType) { case 'proxmox-pve': return resource.sourceType === 'hybrid' ? ['proxmox', 'agent'] : ['proxmox']; case 'docker': return ['docker']; case 'kubernetes': return resource.sourceType === 'hybrid' ? ['agent', 'kubernetes'] : ['kubernetes']; case 'proxmox-pbs': return ['pbs']; case 'proxmox-pmg': return ['pmg']; case 'truenas': return ['truenas']; case 'vmware-vsphere': return ['vmware']; default: return resource.sourceType === 'agent' ? ['agent'] : undefined; } }; const hasLegacyProxmoxShape = ( resource: Resource, platformData: JsonRecord, sources?: string[], ): boolean => resource.platformType === 'proxmox-pve' || sourceListHas(sources, 'proxmox-pve') || [ 'instance', 'node', 'clusterName', 'vmid', 'cpus', 'template', 'swapUsed', 'swapTotal', 'balloon', ].some((key) => platformData[key] !== undefined); const canonicalizeLegacyPlatformData = (resource: Resource): Resource['platformData'] => { const platformData = asRecord(resource.platformData); if (!platformData) { const normalizedSources = deriveLegacySourceList(resource); if (!normalizedSources || normalizedSources.length === 0) { return resource.platformData; } const normalized: JsonRecord = { sources: normalizedSources }; const resourceRecord = getResourceRecord(resource); for (const [key, value] of [ ['agent', resourceRecord.agent], ['docker', resourceRecord.docker], ['proxmox', resourceRecord.proxmox], ['pbs', resourceRecord.pbs], ['pmg', resourceRecord.pmg], ['kubernetes', resourceRecord.kubernetes], ['vmware', resourceRecord.vmware], ['storage', resourceRecord.storage], ['availability', resourceRecord.availability], ['physicalDisk', resourceRecord.physicalDisk], ] as const) { if (value !== undefined) { normalized[key] = value; } } return normalized; } const normalized: JsonRecord = { ...platformData }; const resourceRecord = getResourceRecord(resource); for (const key of [ 'agent', 'docker', 'proxmox', 'pbs', 'pmg', 'kubernetes', 'vmware', 'storage', 'availability', 'physicalDisk', ] as const) { if (!asRecord(normalized[key]) && asRecord(resourceRecord[key])) { normalized[key] = resourceRecord[key]; } } const explicitResourceSources = getExplicitResourceSources(resource); const normalizedSources = explicitResourceSources ?? (Array.isArray(platformData.sources) && platformData.sources.length > 0 ? (platformData.sources as string[]) : deriveLegacySourceList(resource, platformData)); if (normalizedSources && normalizedSources.length > 0) { normalized.sources = normalizedSources; } if (!asRecord(normalized.agent)) { const agentPayload: JsonRecord = {}; for (const [legacyKey, nextKey] of [ ['agentId', 'agentId'], ['agentVersion', 'agentVersion'], ['hostname', 'hostname'], ['platform', 'platform'], ['osName', 'osName'], ['osVersion', 'osVersion'], ['kernelVersion', 'kernelVersion'], ['architecture', 'architecture'], ['commandsEnabled', 'commandsEnabled'], ] as const) { if (platformData[legacyKey] !== undefined) { agentPayload[nextKey] = platformData[legacyKey]; } } if (platformData.memory !== undefined) agentPayload.memory = platformData.memory; if (platformData.interfaces !== undefined) agentPayload.networkInterfaces = platformData.interfaces; if (platformData.disks !== undefined) agentPayload.disks = platformData.disks; if (Object.keys(agentPayload).length > 0) { normalized.agent = agentPayload; } } if (!asRecord(normalized.docker)) { const dockerPayload: JsonRecord = {}; for (const [legacyKey, nextKey] of [ ['agentId', 'agentId'], ['runtime', 'runtime'], ['runtimeVersion', 'runtimeVersion'], ['dockerVersion', 'dockerVersion'], ['os', 'os'], ['kernelVersion', 'kernelVersion'], ['architecture', 'architecture'], ['agentVersion', 'agentVersion'], ['hostname', 'hostname'], ['displayName', 'displayName'], ['machineId', 'machineId'], ['containerCount', 'containerCount'], ['uptimeSeconds', 'uptimeSeconds'], ['intervalSeconds', 'intervalSeconds'], ['temperature', 'temperature'], ['hostSourceId', 'hostSourceId'], ] as const) { if (platformData[legacyKey] !== undefined) { dockerPayload[nextKey] = platformData[legacyKey]; } } if (platformData.swarm !== undefined) dockerPayload.swarm = platformData.swarm; if (platformData.interfaces !== undefined) dockerPayload.networkInterfaces = platformData.interfaces; if (platformData.disks !== undefined) dockerPayload.disks = platformData.disks; if (Object.keys(dockerPayload).length > 0) { normalized.docker = dockerPayload; } } if ( !asRecord(normalized.proxmox) && hasLegacyProxmoxShape(resource, platformData, normalizedSources) ) { const proxmoxPayload: JsonRecord = {}; for (const [legacyKey, nextKey] of [ ['instance', 'instance'], ['node', 'nodeName'], ['clusterName', 'clusterName'], ['vmid', 'vmid'], ['cpus', 'cpus'], ['template', 'template'], ['swapUsed', 'swapUsed'], ['swapTotal', 'swapTotal'], ['balloon', 'balloon'], ] as const) { if (platformData[legacyKey] !== undefined) { proxmoxPayload[nextKey] = platformData[legacyKey]; } } if (platformData.disks !== undefined) proxmoxPayload.disks = platformData.disks; if (Object.keys(proxmoxPayload).length > 0) { normalized.proxmox = proxmoxPayload; } } if (!asRecord(normalized.pbs)) { const pbsPayload: JsonRecord = {}; if (platformData.host !== undefined) pbsPayload.hostname = platformData.host; if (platformData.version !== undefined) pbsPayload.version = platformData.version; if (platformData.connectionHealth !== undefined) { pbsPayload.connectionHealth = platformData.connectionHealth; } if (platformData.numDatastores !== undefined) { pbsPayload.datastoreCount = platformData.numDatastores; } if (Object.keys(pbsPayload).length > 0) { normalized.pbs = pbsPayload; } } if (!asRecord(normalized.pmg)) { const pmgPayload: JsonRecord = {}; if (platformData.host !== undefined) pmgPayload.hostname = platformData.host; if (platformData.version !== undefined) pmgPayload.version = platformData.version; if (platformData.connectionHealth !== undefined) { pmgPayload.connectionHealth = platformData.connectionHealth; } for (const [legacyKey, nextKey] of [ ['nodeCount', 'nodeCount'], ['queueActive', 'queueActive'], ['queueDeferred', 'queueDeferred'], ['queueHold', 'queueHold'], ['queueIncoming', 'queueIncoming'], ['queueTotal', 'queueTotal'], ] as const) { if (platformData[legacyKey] !== undefined) { pmgPayload[nextKey] = platformData[legacyKey]; } } if (Object.keys(pmgPayload).length > 0) { normalized.pmg = pmgPayload; } } if (!asRecord(normalized.kubernetes)) { const kubernetesPayload: JsonRecord = {}; for (const [legacyKey, nextKey] of [ ['agentId', 'agentId'], ['clusterId', 'clusterId'], ['context', 'context'], ['nodeName', 'nodeName'], ['namespace', 'namespace'], ['clusterName', 'clusterName'], ['pendingUninstall', 'pendingUninstall'], ] as const) { if (platformData[legacyKey] !== undefined) { kubernetesPayload[nextKey] = platformData[legacyKey]; } } if (Object.keys(kubernetesPayload).length > 0) { normalized.kubernetes = kubernetesPayload; } } return normalized; }; const getCanonicalSourceList = ( resource: Resource, platformData?: Resource['platformData'], ): string[] | undefined => { const resourceSources = getExplicitResourceSources(resource); if (resourceSources && resourceSources.length > 0) return resourceSources; const platformRecord = asRecord(platformData); return Array.isArray(platformRecord?.sources) && platformRecord.sources.length > 0 ? (platformRecord.sources as string[]) : deriveLegacySourceList(resource, platformData); }; const sourceListContainsRuntimePlatform = (sources: string[] | undefined): boolean => sourceListHas(sources, 'proxmox-pve', 'docker', 'kubernetes', 'vmware-vsphere', 'truenas'); const getHostResourceMergeKey = (resource: Resource): string | undefined => { if (resource.type !== 'agent') return undefined; const platform = asRecord(resource.platformData); const canonical = resource.canonicalIdentity; const candidates = [ canonical?.platformId, canonical?.hostname, resource.platformId, asString(asRecord(resource.agent)?.hostname), asString(asRecord(platform?.agent)?.hostname), asString(asRecord(resource.proxmox)?.nodeName), asString(asRecord(platform?.proxmox)?.nodeName), getPreferredResourceHostname(resource), getPreferredInfrastructureDisplayName(resource), resource.displayName, resource.name, ]; const hostKey = candidates.map(normalizeResourceIdentityToken).find(Boolean); return hostKey ? `agent:${hostKey}` : undefined; }; const shouldMergeRealtimeHostResources = (incoming: Resource, existing: Resource): boolean => { if (incoming.type !== 'agent' || existing.type !== 'agent') return false; const incomingSources = getCanonicalSourceList(incoming, incoming.platformData); const existingSources = getCanonicalSourceList(existing, existing.platformData); const unionSources = mergeStringArrays(incomingSources, existingSources); return sourceListHas(unionSources, 'agent') && sourceListContainsRuntimePlatform(unionSources); }; const preferHostResourcePrimary = (candidate: Resource, other: Resource): boolean => { const candidateSources = getCanonicalSourceList(candidate, candidate.platformData); const otherSources = getCanonicalSourceList(other, other.platformData); if (sourceListHas(candidateSources, 'agent') && !sourceListHas(otherSources, 'agent')) { return true; } if (!sourceListHas(candidateSources, 'agent') && sourceListHas(otherSources, 'agent')) { return false; } return candidate.lastSeen >= other.lastSeen; }; const withMergedSnapshotSources = (resource: Resource, sources: string[] | undefined): Resource => { if (!sources || sources.length === 0) return resource; const platform = asRecord(resource.platformData); return canonicalizeRealtimeResource({ ...resource, sources, platformData: { ...(platform ?? {}), sources, }, }); }; const mergeRealtimeHostResources = (incoming: Resource, existing: Resource): Resource => { const unionSources = mergeStringArrays( getCanonicalSourceList(incoming, incoming.platformData), getCanonicalSourceList(existing, existing.platformData), ); const primary = preferHostResourcePrimary(incoming, existing) ? incoming : existing; const secondary = primary === incoming ? existing : incoming; return canonicalizeRealtimeResource( mergeCanonicalResource( withMergedSnapshotSources(primary, unionSources), withMergedSnapshotSources(secondary, unionSources), ), ); }; const coalesceRealtimeResourceSnapshot = (resources: Resource[]): Resource[] => { const coalesced: Resource[] = []; const indexByHostKey = new Map(); for (const rawResource of resources) { const resource = canonicalizeRealtimeResource(rawResource); const hostKey = getHostResourceMergeKey(resource); if (!hostKey) { coalesced.push(resource); continue; } const existingIndex = indexByHostKey.get(hostKey); if (existingIndex === undefined) { indexByHostKey.set(hostKey, coalesced.length); coalesced.push(resource); continue; } const existing = coalesced[existingIndex]; if (!shouldMergeRealtimeHostResources(resource, existing)) { coalesced.push(resource); continue; } coalesced[existingIndex] = mergeRealtimeHostResources(resource, existing); } return coalesced; }; const hasAvailabilityFacet = ( resource: Resource, platformData?: Resource['platformData'], ): boolean => { const platformRecord = asRecord(platformData); const sources = getCanonicalSourceList(resource, platformData); return ( resource.type === 'network-endpoint' || resource.platformType === 'availability' || Boolean(resource.availability) || Boolean(platformRecord?.availability) || Boolean(sources?.some((source) => source.trim().toLowerCase() === 'availability')) ); }; export const canonicalizeRealtimeResource = (resource: Resource): Resource => { const platformData = canonicalizeLegacyPlatformData(resource); const platformRecord = asRecord(platformData); const sources = getCanonicalSourceList(resource, platformData); const platformType = resolvePlatformTypeFromSources(sources) || (hasAvailabilityFacet(resource, platformData) ? 'availability' : resource.platformType); const sourceType = sources && sources.length > 0 ? resolveSourceTypeFromSources(sources) : resource.sourceType; const normalizedBase = { ...resource, platformType, sourceType, platformData, }; return { ...normalizedBase, clusterId: resource.clusterId ?? getExplicitResourceClusterName(normalizedBase), platformData, agent: resource.agent ?? (platformRecord?.agent as Resource['agent']), proxmox: resource.proxmox ?? (platformRecord?.proxmox as Resource['proxmox']), pbs: resource.pbs ?? (platformRecord?.pbs as Resource['pbs']), kubernetes: resource.kubernetes ?? (platformRecord?.kubernetes as Resource['kubernetes']), vmware: resource.vmware ?? (platformRecord?.vmware as Resource['vmware']), storage: resource.storage ?? (platformRecord?.storage as Resource['storage']), availability: resource.availability ?? (platformRecord?.availability as Resource['availability']), physicalDisk: resource.physicalDisk ?? (platformRecord?.physicalDisk as Resource['physicalDisk']), }; }; const mergeCanonicalIdentity = ( incoming?: Resource['canonicalIdentity'], existing?: Resource['canonicalIdentity'], ): Resource['canonicalIdentity'] => { if (!incoming) return existing; if (!existing) return incoming; const aliases = mergeStringArrays(incoming.aliases, existing.aliases); return { ...existing, ...incoming, aliases, }; }; const mergeCanonicalSourceFacet = ( incomingFacet: T | undefined, existingFacet: T | undefined, incomingSources: string[] | undefined, ...sourceCandidates: string[] ): T | undefined => shouldKeepSourceFacet(incomingSources, ...sourceCandidates) ? mergeRecord(incomingFacet, existingFacet) : incomingFacet; export const mergeCanonicalResource = (incoming: Resource, existing?: Resource): Resource => { if (!existing) { return incoming; } const existingCanonical = canonicalizeRealtimeResource(existing); const incomingSources = getCanonicalSourceList(incoming, incoming.platformData); return { ...existingCanonical, ...incoming, clusterId: incoming.clusterId ?? existingCanonical.clusterId, discoveryTarget: incoming.discoveryTarget ?? existingCanonical.discoveryTarget, metricsTarget: incoming.metricsTarget ?? existingCanonical.metricsTarget, canonicalIdentity: mergeCanonicalIdentity( incoming.canonicalIdentity, existingCanonical.canonicalIdentity, ), policy: incoming.policy ?? existingCanonical.policy, aiSafeSummary: incoming.aiSafeSummary ?? existingCanonical.aiSafeSummary, recentChanges: incoming.recentChanges ?? existingCanonical.recentChanges, facetCounts: incoming.facetCounts ?? existingCanonical.facetCounts, diskIO: incoming.diskIO ?? existingCanonical.diskIO, agent: mergeCanonicalSourceFacet( incoming.agent as JsonRecord | undefined, existingCanonical.agent as JsonRecord | undefined, incomingSources, 'agent', ) as Resource['agent'], proxmox: mergeCanonicalSourceFacet( incoming.proxmox as JsonRecord | undefined, existingCanonical.proxmox as JsonRecord | undefined, incomingSources, 'proxmox-pve', ) as Resource['proxmox'], pbs: mergeCanonicalSourceFacet( incoming.pbs as JsonRecord | undefined, existingCanonical.pbs as JsonRecord | undefined, incomingSources, 'proxmox-pbs', ) as Resource['pbs'], kubernetes: mergeCanonicalSourceFacet( incoming.kubernetes as JsonRecord | undefined, existingCanonical.kubernetes as JsonRecord | undefined, incomingSources, 'kubernetes', ) as Resource['kubernetes'], vmware: mergeCanonicalSourceFacet( incoming.vmware as JsonRecord | undefined, existingCanonical.vmware as JsonRecord | undefined, incomingSources, 'vmware-vsphere', ) as Resource['vmware'], availability: mergeCanonicalSourceFacet( incoming.availability as JsonRecord | undefined, existingCanonical.availability as JsonRecord | undefined, incomingSources, 'availability', ) as Resource['availability'], storage: mergeRecord( incoming.storage as JsonRecord | undefined, existingCanonical.storage as JsonRecord | undefined, ) as Resource['storage'], physicalDisk: mergeRecord( incoming.physicalDisk as JsonRecord | undefined, existingCanonical.physicalDisk as JsonRecord | undefined, ) as Resource['physicalDisk'], identity: mergeRecord( incoming.identity as JsonRecord | undefined, existingCanonical.identity as JsonRecord | undefined, ) as Resource['identity'], platformData: mergePlatformData(incoming.platformData, existingCanonical.platformData), tags: incoming.tags && incoming.tags.length > 0 ? incoming.tags : existingCanonical.tags, labels: incoming.labels && Object.keys(incoming.labels).length > 0 ? incoming.labels : existingCanonical.labels, }; }; export const mergeCanonicalResourceSnapshot = ( incoming: Resource[], existing: Resource[], ): Resource[] => { if (incoming.length === 0) { return []; } const coalescedIncoming = coalesceRealtimeResourceSnapshot(incoming); const existingById = new Map(existing.map((resource) => [resource.id, resource] as const)); return coalescedIncoming.map((resource) => mergeCanonicalResource(resource, existingById.get(resource.id)), ); }; const buildMemory = (metric: Resource['memory'], fallback?: Record): Memory => { const total = metric?.total ?? asNumber(fallback?.total) ?? 0; const used = metric?.used ?? asNumber(fallback?.used) ?? 0; const free = metric?.free ?? asNumber(fallback?.free) ?? Math.max(total - used, 0); const usage = metric?.current ?? (total > 0 ? (used / total) * 100 : (asNumber(fallback?.usage) ?? 0)); return { total, used, free, usage, swapUsed: asNumber(fallback?.swapUsed), swapTotal: asNumber(fallback?.swapTotal), balloon: asNumber(fallback?.balloon), }; }; const buildDisk = (metric: Resource['disk'], fallback?: Record): Disk => { const total = metric?.total ?? asNumber(fallback?.total) ?? 0; const used = metric?.used ?? asNumber(fallback?.used) ?? 0; const free = metric?.free ?? asNumber(fallback?.free) ?? Math.max(total - used, 0); const usage = metric?.current ?? (total > 0 ? (used / total) * 100 : (asNumber(fallback?.usage) ?? 0)); return { total, used, free, usage, mountpoint: asString(fallback?.mountpoint), type: asString(fallback?.type), device: asString(fallback?.device), }; }; const buildTemperature = ( resource: Resource, nodeMeta?: Record, ): Temperature | undefined => { const platform = resourcePlatformData(resource); const raw = asRecord(platform?.temperature) || asRecord(nodeMeta?.temperature) || asRecord(platform?.agent) || undefined; if (raw) { const available = asBoolean(raw.available); const cpuPackage = asNumber(raw.cpuPackage) ?? asNumber(raw.temperature) ?? asNumber(raw.cpu); const lastUpdate = toISOTime(raw.lastUpdate, resource.lastSeen); if (available || typeof cpuPackage === 'number') { return { cpuPackage, cpuMax: asNumber(raw.cpuMax), cpuMin: asNumber(raw.cpuMin), cpuMaxRecord: asNumber(raw.cpuMaxRecord), minRecorded: asString(raw.minRecorded), maxRecorded: asString(raw.maxRecorded), cores: asArray(raw.cores) .map((entry) => { const rec = asRecord(entry); if (!rec) return null; const core = asNumber(rec.core); const temp = asNumber(rec.temp); if (typeof core !== 'number' || typeof temp !== 'number') return null; return { core, temp }; }) .filter((entry): entry is NonNullable => Boolean(entry)), gpu: asArray(raw.gpu) .map((entry) => { const rec = asRecord(entry); if (!rec) return null; const device = asString(rec.device); if (!device) return null; return { device, edge: asNumber(rec.edge), junction: asNumber(rec.junction), mem: asNumber(rec.mem), }; }) .filter((entry): entry is NonNullable => Boolean(entry)), nvme: asArray(raw.nvme) .map((entry) => { const rec = asRecord(entry); if (!rec) return null; const device = asString(rec.device); const temp = asNumber(rec.temp); if (!device || typeof temp !== 'number') return null; return { device, temp }; }) .filter((entry): entry is NonNullable => Boolean(entry)), available: available ?? true, hasCPU: asBoolean(raw.hasCPU) ?? (typeof cpuPackage === 'number' ? true : undefined), hasGPU: asBoolean(raw.hasGPU), hasNVMe: asBoolean(raw.hasNVMe), lastUpdate, }; } } if (typeof resource.temperature === 'number' && Number.isFinite(resource.temperature)) { const temp = resource.temperature; return { cpuPackage: temp, cpuMax: temp, cpuMin: temp, cpuMaxRecord: temp, available: true, hasCPU: true, lastUpdate: toISOTime(undefined, resource.lastSeen), }; } return undefined; }; export const nodeFromResource = (resource: Resource): Node | null => { if (resource.type !== 'agent') return null; const platform = resourcePlatformData(resource); const proxmox = asRecord(platform?.proxmox) || (resource.proxmox as unknown as Record | undefined); const cpuInfo = asRecord(proxmox?.cpuInfo); const preferredHostLabel = getPreferredResourceHostname(resource) || getPreferredInfrastructureDisplayName(resource) || resource.id; const instance = asString(proxmox?.instance) || resource.platformId || getCanonicalPlatformId(resource) || preferredHostLabel; const name = asString(proxmox?.nodeName) || asString(proxmox?.node) || preferredHostLabel; const linkedAgentId = asString(platform?.linkedAgentId) || getActionableAgentIdFromResource(resource); const agentFacet = resource.agent; const pveVersion = asString(proxmox?.pveVersion) || ((agentFacet?.osName || '').toLowerCase().includes('proxmox') ? asString(agentFacet?.osVersion) : '') || 'Unknown'; return { id: resource.id, name, displayName: getPreferredInfrastructureDisplayName(resource), instance, host: name || preferredHostLabel, guestURL: asString((resource as unknown as Record).customURL) || asString((resource as unknown as Record).customUrl), status: resource.status || 'unknown', type: resource.type, cpu: resource.cpu?.current ?? 0, memory: buildMemory(resource.memory, asRecord(proxmox?.memory)), disk: buildDisk(resource.disk, asRecord(proxmox?.disk)), networkIn: resource.network?.rxBytes, networkOut: resource.network?.txBytes, diskRead: resource.diskIO?.readRate, diskWrite: resource.diskIO?.writeRate, uptime: resource.uptime ?? asNumber(proxmox?.uptime) ?? 0, loadAverage: asArray(proxmox?.loadAverage) .map((value) => asNumber(value)) .filter((value): value is number => typeof value === 'number'), kernelVersion: asString(proxmox?.kernelVersion) || 'Unknown', pveVersion, cpuInfo: { model: asString(cpuInfo?.model) || 'Unknown', cores: asNumber(cpuInfo?.cores) ?? 0, sockets: asNumber(cpuInfo?.sockets) ?? 0, mhz: asString(cpuInfo?.mhz) || '0', }, temperature: buildTemperature(resource, proxmox), temperatureMonitoringEnabled: asBoolean(platform?.temperatureMonitoringEnabled) ?? asBoolean(proxmox?.temperatureMonitoringEnabled) ?? null, pendingUpdates: asNumber(proxmox?.pendingUpdates), pendingUpdatesCheckedAt: asString(proxmox?.pendingUpdatesCheckedAt), lastSeen: toISOTime(undefined, resource.lastSeen), connectionHealth: asString(proxmox?.connectionHealth) || resource.status || 'unknown', isClusterMember: asBoolean(proxmox?.isClusterMember), clusterName: getPreferredResourceClusterName(resource), linkedAgentId, }; }; const mapPBSNamespace = (value: unknown): PBSNamespace | null => { const rec = asRecord(value); if (!rec) return null; return { path: asString(rec.path) || '', parent: asString(rec.parent) || '', depth: asNumber(rec.depth) ?? 0, }; }; const mapPBSDatastore = (value: unknown): PBSDatastore | null => { const rec = asRecord(value); if (!rec) return null; const total = asNumber(rec.total) ?? 0; const used = asNumber(rec.used) ?? 0; const free = asNumber(rec.free) ?? asNumber(rec.available) ?? Math.max(total - used, 0); const usage = asNumber(rec.usage) ?? asNumber(rec.usagePercent) ?? (total > 0 ? (used / total) * 100 : 0); return { name: asString(rec.name) || '', total, used, free, usage, status: asString(rec.status) || '', error: asString(rec.error) || '', namespaces: asArray(rec.namespaces) .map(mapPBSNamespace) .filter((entry): entry is PBSNamespace => Boolean(entry)), deduplicationFactor: asNumber(rec.deduplicationFactor), }; }; const mapPBSBackupJob = (value: unknown): PBSBackupJob | null => { const rec = asRecord(value); if (!rec) return null; return { id: asString(rec.id) || '', store: asString(rec.store) || '', type: asString(rec.type) || '', vmid: asString(rec.vmid) || '', lastBackup: asString(rec.lastBackup) || '', nextRun: asString(rec.nextRun) || '', status: asString(rec.status) || '', error: asString(rec.error) || '', }; }; const mapPBSSyncJob = (value: unknown): PBSSyncJob | null => { const rec = asRecord(value); if (!rec) return null; return { id: asString(rec.id) || '', store: asString(rec.store) || '', remote: asString(rec.remote) || '', status: asString(rec.status) || '', lastSync: asString(rec.lastSync) || '', nextRun: asString(rec.nextRun) || '', error: asString(rec.error) || '', }; }; const mapPBSVerifyJob = (value: unknown): PBSVerifyJob | null => { const rec = asRecord(value); if (!rec) return null; return { id: asString(rec.id) || '', store: asString(rec.store) || '', status: asString(rec.status) || '', lastVerify: asString(rec.lastVerify) || '', nextRun: asString(rec.nextRun) || '', error: asString(rec.error) || '', }; }; const mapPBSPruneJob = (value: unknown): PBSPruneJob | null => { const rec = asRecord(value); if (!rec) return null; return { id: asString(rec.id) || '', store: asString(rec.store) || '', status: asString(rec.status) || '', lastPrune: asString(rec.lastPrune) || '', nextRun: asString(rec.nextRun) || '', error: asString(rec.error) || '', }; }; const mapPBSGarbageJob = (value: unknown): PBSGarbageJob | null => { const rec = asRecord(value); if (!rec) return null; return { id: asString(rec.id) || '', store: asString(rec.store) || '', status: asString(rec.status) || '', lastGarbage: asString(rec.lastGarbage) || '', nextRun: asString(rec.nextRun) || '', removedBytes: asNumber(rec.removedBytes) ?? 0, error: asString(rec.error) || '', }; }; export const pbsInstanceFromResource = (resource: Resource): PBSInstance | null => { if (resource.type !== 'pbs') return null; const platform = resourcePlatformData(resource); const pbs = asRecord(platform?.pbs); const memoryTotal = resource.memory?.total ?? asNumber(pbs?.memoryTotal) ?? 0; const memoryUsed = resource.memory?.used ?? asNumber(pbs?.memoryUsed) ?? 0; const cpu = resource.cpu?.current ?? asNumber(pbs?.cpuPercent) ?? 0; const memoryPercent = resource.memory?.current ?? (memoryTotal > 0 ? (memoryUsed / memoryTotal) * 100 : 0); const hostName = getPreferredResourceHostname(resource) || resource.id; const host = resource.platformId || `https://${hostName}:8007`; return { id: asString(pbs?.instanceId) || resource.id, name: getPreferredInfrastructureDisplayName(resource), host, guestURL: asString((resource as unknown as Record).customURL) || asString((resource as unknown as Record).customUrl), status: resource.status || 'unknown', version: asString(pbs?.version) || '', cpu, memory: memoryPercent, memoryUsed, memoryTotal, uptime: resource.uptime ?? asNumber(pbs?.uptimeSeconds) ?? 0, datastores: asArray(pbs?.datastores) .map(mapPBSDatastore) .filter((entry): entry is PBSDatastore => Boolean(entry)), backupJobs: asArray(pbs?.backupJobs) .map(mapPBSBackupJob) .filter((entry): entry is PBSBackupJob => Boolean(entry)), syncJobs: asArray(pbs?.syncJobs) .map(mapPBSSyncJob) .filter((entry): entry is PBSSyncJob => Boolean(entry)), verifyJobs: asArray(pbs?.verifyJobs) .map(mapPBSVerifyJob) .filter((entry): entry is PBSVerifyJob => Boolean(entry)), pruneJobs: asArray(pbs?.pruneJobs) .map(mapPBSPruneJob) .filter((entry): entry is PBSPruneJob => Boolean(entry)), garbageJobs: asArray(pbs?.garbageJobs) .map(mapPBSGarbageJob) .filter((entry): entry is PBSGarbageJob => Boolean(entry)), connectionHealth: asString(pbs?.connectionHealth) || resource.status || 'unknown', lastSeen: toISOTime(undefined, resource.lastSeen), }; }; const mapPMGNodeStatus = (value: unknown): PMGNodeStatus | null => { const rec = asRecord(value); if (!rec) return null; const queue = asRecord(rec.queueStatus); return { name: asString(rec.name) || '', status: asString(rec.status) || '', role: asString(rec.role), uptime: asNumber(rec.uptime), loadAvg: asString(rec.loadAvg), queueStatus: queue ? { active: asNumber(queue.active) ?? 0, deferred: asNumber(queue.deferred) ?? 0, hold: asNumber(queue.hold) ?? 0, incoming: asNumber(queue.incoming) ?? 0, total: asNumber(queue.total) ?? 0, oldestAge: asNumber(queue.oldestAge) ?? 0, updatedAt: asString(queue.updatedAt) || '', } : undefined, }; }; const mapPMGMailStats = (value: unknown): PMGMailStats | undefined => { const rec = asRecord(value); if (!rec) return undefined; return { timeframe: asString(rec.timeframe) || '', countTotal: asNumber(rec.countTotal) ?? 0, countIn: asNumber(rec.countIn) ?? 0, countOut: asNumber(rec.countOut) ?? 0, spamIn: asNumber(rec.spamIn) ?? 0, spamOut: asNumber(rec.spamOut) ?? 0, virusIn: asNumber(rec.virusIn) ?? 0, virusOut: asNumber(rec.virusOut) ?? 0, bouncesIn: asNumber(rec.bouncesIn) ?? 0, bouncesOut: asNumber(rec.bouncesOut) ?? 0, bytesIn: asNumber(rec.bytesIn) ?? 0, bytesOut: asNumber(rec.bytesOut) ?? 0, greylistCount: asNumber(rec.greylistCount) ?? 0, junkIn: asNumber(rec.junkIn) ?? 0, averageProcessTimeMs: asNumber(rec.averageProcessTimeMs) ?? 0, rblRejects: asNumber(rec.rblRejects) ?? 0, pregreetRejects: asNumber(rec.pregreetRejects) ?? 0, updatedAt: toISOTime(rec.updatedAt), }; }; const mapPMGQuarantine = (value: unknown): PMGQuarantineTotals | undefined => { const rec = asRecord(value); if (!rec) return undefined; return { spam: asNumber(rec.spam) ?? 0, virus: asNumber(rec.virus) ?? 0, attachment: asNumber(rec.attachment) ?? 0, blacklisted: asNumber(rec.blacklisted) ?? 0, }; }; const mapPMGSpamBucket = (value: unknown): PMGSpamBucket | null => { const rec = asRecord(value); if (!rec) return null; return { score: asString(rec.score) || asString(rec.bucket) || '', count: asNumber(rec.count) ?? 0, }; }; const mapPMGRelayDomain = (value: unknown): PMGRelayDomain | null => { const rec = asRecord(value); if (!rec) return null; return { domain: asString(rec.domain) || '', comment: asString(rec.comment), }; }; const mapPMGDomainStat = (value: unknown): PMGDomainStat | null => { const rec = asRecord(value); if (!rec) return null; return { domain: asString(rec.domain) || '', mailCount: asNumber(rec.mailCount) ?? 0, spamCount: asNumber(rec.spamCount) ?? 0, virusCount: asNumber(rec.virusCount) ?? 0, bytes: asNumber(rec.bytes), }; }; const mapPMGMailCountPoint = (value: unknown): PMGMailCountPoint | null => { const rec = asRecord(value); if (!rec) return null; return { timestamp: toISOTime(rec.timestamp), count: asNumber(rec.count) ?? 0, countIn: asNumber(rec.countIn) ?? 0, countOut: asNumber(rec.countOut) ?? 0, spamIn: asNumber(rec.spamIn) ?? 0, spamOut: asNumber(rec.spamOut) ?? 0, virusIn: asNumber(rec.virusIn) ?? 0, virusOut: asNumber(rec.virusOut) ?? 0, rblRejects: asNumber(rec.rblRejects) ?? 0, pregreet: asNumber(rec.pregreet) ?? 0, bouncesIn: asNumber(rec.bouncesIn) ?? 0, bouncesOut: asNumber(rec.bouncesOut) ?? 0, greylist: asNumber(rec.greylist) ?? 0, index: asNumber(rec.index) ?? 0, timeframe: asString(rec.timeframe) || '', windowStart: asString(rec.windowStart), windowEnd: asString(rec.windowEnd), }; }; export const pmgInstanceFromResource = (resource: Resource): PMGInstance | null => { if (resource.type !== 'pmg') return null; const platform = resourcePlatformData(resource); const pmg = asRecord(platform?.pmg); const hostName = getPreferredResourceHostname(resource) || resource.id; const host = resource.platformId || `https://${hostName}:8006`; const lastSeen = toISOTime(undefined, resource.lastSeen); const mailStats = mapPMGMailStats(pmg?.mailStats) || mapPMGMailStats({ countTotal: asNumber(pmg?.mailCountTotal), spamIn: asNumber(pmg?.spamIn), virusIn: asNumber(pmg?.virusIn), updatedAt: pmg?.lastUpdated, }); return { id: asString(pmg?.instanceId) || resource.id, name: getPreferredInfrastructureDisplayName(resource), host, guestURL: asString((resource as unknown as Record).customURL) || asString((resource as unknown as Record).customUrl), status: resource.status || 'unknown', version: asString(pmg?.version) || '', nodes: asArray(pmg?.nodes) .map(mapPMGNodeStatus) .filter((entry): entry is PMGNodeStatus => Boolean(entry)), mailStats, mailCount: asArray(pmg?.mailCount) .map(mapPMGMailCountPoint) .filter((entry): entry is PMGMailCountPoint => Boolean(entry)), spamDistribution: asArray(pmg?.spamDistribution) .map(mapPMGSpamBucket) .filter((entry): entry is PMGSpamBucket => Boolean(entry)), quarantine: mapPMGQuarantine(pmg?.quarantine), relayDomains: asArray(pmg?.relayDomains) .map(mapPMGRelayDomain) .filter((entry): entry is PMGRelayDomain => Boolean(entry)), domainStats: asArray(pmg?.domainStats) .map(mapPMGDomainStat) .filter((entry): entry is PMGDomainStat => Boolean(entry)), domainStatsAsOf: toISOTime(pmg?.domainStatsAsOf), connectionHealth: asString(pmg?.connectionHealth) || resource.status || 'unknown', lastSeen, lastUpdated: toISOTime(pmg?.lastUpdated, resource.lastSeen), }; };