import { memo, useCallback, useMemo, useState } from "react"; import { Activity, Box, Play, RotateCw, Server, Settings, Shield, Square, Trash2 } from "lucide-react"; import type { ManagedDockerNodeInfo, NodeInfo, ProjectInfo } from "../api"; import { getProjectCountForNode } from "../utils/nodeProjectAssignment"; import type { ComputedNodeSyncStatus } from "../hooks/useNodeSettingsSync"; import { formatRelativeTime, getSyncStateColor } from "../hooks/useNodeSettingsSync"; export interface NodeCardProps { node: NodeInfo; projects: ProjectInfo[]; onHealthCheck: (id: string) => void; onEdit: (node: NodeInfo) => void; onRemove: (id: string) => void; isLoading?: boolean; syncStatus?: ComputedNodeSyncStatus; /** Auth credential sync state for this node. Only meaningful for remote nodes. */ authSyncState?: "match" | "differs" | "not-synced"; /** Per-provider auth match details for tooltip. Map of provider name (e.g. "anthropic") to its match status. */ authSyncProviders?: Record; managedDockerNode?: ManagedDockerNodeInfo; } const STATUS_CONFIG: Record = { online: { label: "Online", color: "var(--color-success)", className: "node-card__status--online" }, offline: { label: "Offline", color: "var(--color-error)", className: "node-card__status--offline" }, connecting: { label: "Connecting", color: "var(--color-warning)", className: "node-card__status--connecting" }, error: { label: "Error", color: "var(--color-error)", className: "node-card__status--error" }, creating: { label: "Creating", color: "var(--color-warning)", className: "node-card__status--creating" }, recreating: { label: "Recreating", color: "var(--color-warning)", className: "node-card__status--recreating" }, deleting: { label: "Deleting", color: "var(--color-error)", className: "node-card__status--deleting" }, running: { label: "Running", color: "var(--color-success)", className: "node-card__status--online" }, stopped: { label: "Stopped", color: "var(--color-error)", className: "node-card__status--offline" }, exited: { label: "Exited", color: "var(--color-error)", className: "node-card__status--offline" }, }; const AUTH_SYNC_COLORS: Record = { match: "var(--color-success)", differs: "var(--color-warning)", "not-synced": "var(--text-muted)", }; function buildAuthTooltip( state: "match" | "differs" | "not-synced", providers?: Record, ): string { if (state === "match") return "Auth credentials match"; if (state === "not-synced") return "Auth not synced"; // state === "differs" if (providers && Object.keys(providers).length > 0) { const differing = Object.entries(providers) .filter(([, status]) => status === "differs") .map(([name]) => name); if (differing.length > 0) { return `Auth credentials differ: ${differing.join(", ")}`; } } return "Auth credentials differ"; } function truncateUrl(url: string, maxLength: number = 42): string { if (url.length <= maxLength) return url; return `${url.slice(0, maxLength - 3)}...`; } function areNodeCardPropsEqual(previous: NodeCardProps, next: NodeCardProps): boolean { const prevNode = previous.node; const nextNode = next.node; if (prevNode.id !== nextNode.id) return false; if (prevNode.name !== nextNode.name) return false; if (prevNode.type !== nextNode.type) return false; if (prevNode.url !== nextNode.url) return false; if (prevNode.status !== nextNode.status) return false; if (prevNode.maxConcurrent !== nextNode.maxConcurrent) return false; if (prevNode.updatedAt !== nextNode.updatedAt) return false; if (previous.isLoading !== next.isLoading) return false; const prevDocker = previous.managedDockerNode; const nextDocker = next.managedDockerNode; if (!!prevDocker !== !!nextDocker) return false; if (prevDocker && nextDocker) { if (prevDocker.id !== nextDocker.id) return false; if (prevDocker.status !== nextDocker.status) return false; if (prevDocker.imageTag !== nextDocker.imageTag) return false; if (prevDocker.updatedAt !== nextDocker.updatedAt) return false; } // Compare sync status const prevSync = previous.syncStatus; const nextSync = next.syncStatus; if (!prevSync && !nextSync) { // Both undefined - equal } else if (!prevSync || !nextSync) { return false; // One defined, one not } else { if (prevSync.syncState !== nextSync.syncState) return false; if (prevSync.lastSyncAt !== nextSync.lastSyncAt) return false; if (prevSync.diffCount !== nextSync.diffCount) return false; } // Compare auth sync state if (previous.authSyncState !== next.authSyncState) return false; // Shallow compare authSyncProviders const prevProviders = previous.authSyncProviders; const nextProviders = next.authSyncProviders; if (prevProviders === nextProviders) { // same ref or both undefined - equal } else if (!prevProviders || !nextProviders) { return false; // one defined, one not } else { const prevKeys = Object.keys(prevProviders); const nextKeys = Object.keys(nextProviders); if (prevKeys.length !== nextKeys.length) return false; if (prevKeys.some((k) => prevProviders[k] !== nextProviders[k])) return false; } // Compare project counts using the canonical counting function const previousCount = getProjectCountForNode(previous.projects, prevNode); const nextCount = getProjectCountForNode(next.projects, nextNode); return previousCount === nextCount; } function NodeCardInner({ node, projects, onHealthCheck, onEdit, onRemove, isLoading = false, syncStatus, authSyncState, authSyncProviders, managedDockerNode, }: NodeCardProps) { const [removeArmed, setRemoveArmed] = useState(false); const statusConfig = STATUS_CONFIG[node.status] ?? STATUS_CONFIG.offline; const dockerStatusConfig = managedDockerNode ? (STATUS_CONFIG[managedDockerNode.status] ?? STATUS_CONFIG.error) : null; const dockerHost = managedDockerNode?.hostConfig.type === "remote" ? `Remote: ${managedDockerNode.hostConfig.host ?? "unknown"}` : "Local Docker"; const assignedProjectCount = useMemo(() => { return getProjectCountForNode(projects, node); }, [projects, node]); const handleOpenDetails = useCallback(() => { onEdit(node); }, [onEdit, node]); const handleHealthCheck = useCallback((event: React.MouseEvent) => { event.stopPropagation(); onHealthCheck(node.id); }, [onHealthCheck, node.id]); const handleEdit = useCallback((event: React.MouseEvent) => { event.stopPropagation(); onEdit(node); }, [onEdit, node]); const handleRemove = useCallback((event: React.MouseEvent) => { event.stopPropagation(); if (!removeArmed) { setRemoveArmed(true); return; } onRemove(node.id); setRemoveArmed(false); }, [removeArmed, onRemove, node.id]); const handleCardKeyDown = useCallback((event: React.KeyboardEvent) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onEdit(node); } }, [onEdit, node]); return (

{node.name}

{node.type === "local" ? "Local" : "Remote"} {managedDockerNode && ( Docker )} {statusConfig.label} {managedDockerNode && dockerStatusConfig && ( {dockerStatusConfig.label} )} {node.type === "remote" && authSyncState && ( )}
{node.type === "remote" && node.url && (
{truncateUrl(node.url)}
)} {managedDockerNode && (
{managedDockerNode.imageName}:{managedDockerNode.imageTag} {dockerHost}
)}
Projects {assignedProjectCount}
Concurrency {node.maxConcurrent}
{/* Sync status indicator — only for remote nodes with sync data */} {node.type === "remote" && syncStatus && (
{formatRelativeTime(syncStatus.lastSyncAt)}
)}
); } export const NodeCard = memo(NodeCardInner, areNodeCardPropsEqual);