import { Component, For, Show, createMemo } from 'solid-js'; import type { NodeConfig } from '@/types/nodes'; type NodeConfigWithStatus = NodeConfig & { hasPassword?: boolean; hasToken?: boolean; status: 'connected' | 'disconnected' | 'offline' | 'error' | 'pending'; }; export interface TemperatureTransportInfo { httpMap: Record; socketStatus: 'healthy' | 'error' | 'missing'; } interface PveNodesTableProps { nodes: NodeConfigWithStatus[]; stateNodes: { instance: string; status?: string; connectionHealth?: string }[]; globalTemperatureMonitoringEnabled?: boolean; temperatureTransports?: TemperatureTransportInfo | null; onTestConnection: (nodeId: string) => void; onEdit: (node: NodeConfigWithStatus) => void; onDelete: (node: NodeConfigWithStatus) => void; } type StatusMeta = { dotClass: string; label: string; labelClass: string }; type TemperatureTransportBadge = { label: string; badgeClass: string; description?: string; }; const STATUS_META: Record = { online: { dotClass: 'bg-green-500', label: 'Online', labelClass: 'text-green-600 dark:text-green-400', }, offline: { dotClass: 'bg-red-500', label: 'Offline', labelClass: 'text-red-600 dark:text-red-400', }, degraded: { dotClass: 'bg-yellow-500', label: 'Degraded', labelClass: 'text-amber-600 dark:text-amber-400', }, pending: { dotClass: 'bg-amber-500 animate-pulse', label: 'Pending', labelClass: 'text-amber-600 dark:text-amber-400', }, unknown: { dotClass: 'bg-gray-400', label: 'Unknown', labelClass: 'text-gray-500 dark:text-gray-400', }, }; const resolveTemperatureTransport = ( node: NodeConfigWithStatus, info: TemperatureTransportInfo | null | undefined, globalEnabled: boolean, ): TemperatureTransportBadge => { const monitoringEnabled = isTemperatureMonitoringEnabled(node, globalEnabled); if (!monitoringEnabled) { return { label: 'Temp disabled', badgeClass: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300', }; } const key = (node.name || '').toLowerCase(); const httpEntry = info?.httpMap?.[key]; if (httpEntry) { if (httpEntry.reachable) { return { label: 'HTTPS proxy', badgeClass: 'bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300', description: httpEntry.url, }; } return { label: 'HTTPS error', badgeClass: 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300', description: httpEntry.error || 'Proxy unreachable', }; } if (info) { if (info.socketStatus === 'healthy') { return { label: 'Socket proxy', badgeClass: 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300', }; } if (info.socketStatus === 'error') { return { label: 'Socket error', badgeClass: 'bg-amber-100 dark:bg-amber-900 text-amber-700 dark:text-amber-300', description: 'Proxy socket not responding', }; } } return { label: 'SSH fallback', badgeClass: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300', }; }; const isTemperatureMonitoringEnabled = ( node: NodeConfigWithStatus, globalEnabled: boolean, ): boolean => { // Check per-node setting first, fall back to global if (node.temperatureMonitoringEnabled !== undefined && node.temperatureMonitoringEnabled !== null) { return node.temperatureMonitoringEnabled; } return globalEnabled; }; const resolvePveStatusMeta = ( node: NodeConfigWithStatus, stateNodes: PveNodesTableProps['stateNodes'], ): StatusMeta => { const stateNode = stateNodes.find((n) => n.instance === node.name); if ( stateNode?.connectionHealth === 'unhealthy' || stateNode?.connectionHealth === 'error' || stateNode?.status === 'offline' || stateNode?.status === 'disconnected' ) { return STATUS_META.offline; } if (stateNode?.connectionHealth === 'degraded') { return STATUS_META.degraded; } if (stateNode && (stateNode.status === 'online' || stateNode.connectionHealth === 'healthy')) { return STATUS_META.online; } switch (node.status) { case 'connected': return STATUS_META.online; case 'pending': return STATUS_META.pending; case 'disconnected': case 'offline': case 'error': return STATUS_META.offline; default: return STATUS_META.unknown; } }; export const PveNodesTable: Component = (props) => { return (
{(node) => { const statusMeta = createMemo(() => resolvePveStatusMeta(node, props.stateNodes)); const clusterEndpoints = createMemo(() => 'clusterEndpoints' in node && node.clusterEndpoints ? node.clusterEndpoints : [], ); const clusterName = createMemo(() => 'clusterName' in node && node.clusterName ? node.clusterName : 'Unknown', ); const transportMeta = createMemo(() => resolveTemperatureTransport( node, props.temperatureTransports, props.globalTemperatureMonitoringEnabled ?? true, ), ); return ( ); }}
Node Credentials Capabilities Status Actions

{node.name}

{node.host}

{clusterName()} Cluster {clusterEndpoints().length} nodes
0}>
{(endpoint) => { const pulseStatus = endpoint.PulseReachable === null || endpoint.PulseReachable === undefined ? 'unknown' : endpoint.PulseReachable ? 'reachable' : 'unreachable'; const statusColor = endpoint.Online && pulseStatus === 'reachable' ? 'border-green-200 bg-green-50 text-green-700 dark:border-green-700 dark:bg-green-900/20 dark:text-green-300' : pulseStatus === 'unreachable' ? 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-700 dark:bg-amber-900/20 dark:text-amber-300' : endpoint.Online ? 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300' : 'border-gray-200 bg-gray-100 text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400'; return (
{endpoint.NodeName} {endpoint.IP}
Proxmox: {endpoint.Online ? 'Online' : 'Offline'}
Pulse: {pulseStatus === 'reachable' ? 'Reachable' : pulseStatus === 'unreachable' ? 'Unreachable' : 'Checking...'}
Error: {endpoint.PulseError}
); }}

Automatic failover enabled between cluster nodes

{node.user ? `User: ${node.user}` : `Token: ${node.tokenName}`}
{node.type === 'pve' && 'monitorVMs' in node && node.monitorVMs && ( VMs )} {node.type === 'pve' && 'monitorContainers' in node && node.monitorContainers && ( Containers )} {node.type === 'pve' && 'monitorStorage' in node && node.monitorStorage && ( Storage )} {node.type === 'pve' && 'monitorBackups' in node && node.monitorBackups && ( Backups )} {node.type === 'pve' && 'monitorPhysicalDisks' in node && node.monitorPhysicalDisks && ( Physical Disks )} {node.type === 'pve' && isTemperatureMonitoringEnabled(node, props.globalTemperatureMonitoringEnabled ?? true) && ( Temperature )} {transportMeta().label}
{transportMeta()?.description}
{statusMeta().label}
); }; interface PbsNodesTableProps { nodes: NodeConfigWithStatus[]; statePbs: { name: string; status?: string; connectionHealth?: string }[]; globalTemperatureMonitoringEnabled?: boolean; onTestConnection: (nodeId: string) => void; onEdit: (node: NodeConfigWithStatus) => void; onDelete: (node: NodeConfigWithStatus) => void; } const resolvePbsStatusMeta = ( node: NodeConfigWithStatus, statePbs: PbsNodesTableProps['statePbs'], ): StatusMeta => { const statePBS = statePbs.find((p) => p.name === node.name); if ( statePBS?.connectionHealth === 'unhealthy' || statePBS?.connectionHealth === 'error' || statePBS?.status === 'offline' || statePBS?.status === 'disconnected' ) { return STATUS_META.offline; } if (statePBS?.connectionHealth === 'degraded') { return STATUS_META.degraded; } if (statePBS && (statePBS.status === 'online' || statePBS.connectionHealth === 'healthy')) { return STATUS_META.online; } switch (node.status) { case 'connected': return STATUS_META.online; case 'pending': return STATUS_META.pending; case 'disconnected': case 'offline': case 'error': return STATUS_META.offline; default: return STATUS_META.unknown; } }; export const PbsNodesTable: Component = (props) => { return (
{(node) => { const statusMeta = createMemo(() => resolvePbsStatusMeta(node, props.statePbs)); return ( ); }}
Node Credentials Capabilities Status Actions

{node.name}

{node.host}

{node.user ? `User: ${node.user}` : `Token: ${node.tokenName}`}
{node.type === 'pbs' && 'monitorDatastores' in node && node.monitorDatastores && ( Datastores )} {node.type === 'pbs' && 'monitorSyncJobs' in node && node.monitorSyncJobs && ( Sync Jobs )} {node.type === 'pbs' && 'monitorVerifyJobs' in node && node.monitorVerifyJobs && ( Verify Jobs )} {node.type === 'pbs' && 'monitorPruneJobs' in node && node.monitorPruneJobs && ( Prune Jobs )} {node.type === 'pbs' && 'monitorGarbageJobs' in node && (node as NodeConfig & { monitorGarbageJobs?: boolean }).monitorGarbageJobs && ( Garbage Collection )} {node.type === 'pbs' && isTemperatureMonitoringEnabled(node, props.globalTemperatureMonitoringEnabled ?? true) && ( Temperature )}
{statusMeta().label}
); }; interface PmgNodesTableProps { nodes: NodeConfigWithStatus[]; statePmg: { name: string; status?: string; connectionHealth?: string }[]; globalTemperatureMonitoringEnabled?: boolean; onTestConnection: (nodeId: string) => void; onEdit: (node: NodeConfigWithStatus) => void; onDelete: (node: NodeConfigWithStatus) => void; } const resolvePmgStatusMeta = ( node: NodeConfigWithStatus, statePmg: PmgNodesTableProps['statePmg'], ): StatusMeta => { const statePMG = statePmg.find((p) => p.name === node.name); if ( statePMG?.connectionHealth === 'unhealthy' || statePMG?.connectionHealth === 'error' || statePMG?.status === 'offline' || statePMG?.status === 'disconnected' ) { return STATUS_META.offline; } if (statePMG?.connectionHealth === 'degraded') { return STATUS_META.degraded; } if (statePMG && (statePMG.status === 'online' || statePMG.connectionHealth === 'healthy')) { return STATUS_META.online; } switch (node.status) { case 'connected': return STATUS_META.online; case 'pending': return STATUS_META.pending; case 'disconnected': case 'offline': case 'error': return STATUS_META.offline; default: return STATUS_META.unknown; } }; export const PmgNodesTable: Component = (props) => { return (
{(node) => { const statusMeta = createMemo(() => resolvePmgStatusMeta(node, props.statePmg)); return ( ); }}
Node Credentials Capabilities Status Actions

{node.name}

{node.host}

{node.user ? `User: ${node.user}` : `Token: ${node.tokenName}`}
{node.type === 'pmg' && (node as NodeConfig & { monitorMailStats?: boolean }).monitorMailStats && ( Mail stats )} {node.type === 'pmg' && (node as NodeConfig & { monitorQueues?: boolean }).monitorQueues && ( Queues )} {node.type === 'pmg' && (node as NodeConfig & { monitorQuarantine?: boolean }).monitorQuarantine && ( Quarantine )} {node.type === 'pmg' && (node as NodeConfig & { monitorDomainStats?: boolean }).monitorDomainStats && ( Domain stats )}
{statusMeta().label}
); };