import { memo, useCallback, useState } from "react"; import { Play, Pause, AlertCircle, Loader2, Trash2, Folder, ArrowRight } from "lucide-react"; import "./ProjectCard.css"; import type { RegisteredProject, ProjectHealth, ProjectStatus } from "@fusion/core"; import type { ProjectNodeAvailability } from "../api"; export interface ProjectCardProps { project: RegisteredProject; health: ProjectHealth | null; onSelect: (project: RegisteredProject) => void; onPause: (project: RegisteredProject) => void; onResume: (project: RegisteredProject) => void; onRemove: (project: RegisteredProject) => void; availabilityMappings?: Array; isLoading?: boolean; } const STATUS_CONFIG: Record = { active: { label: "Active", color: "var(--color-success)", icon: Play }, paused: { label: "Paused", color: "var(--color-warning)", icon: Pause }, errored: { label: "Error", color: "var(--color-error)", icon: AlertCircle }, initializing: { label: "Initializing", color: "var(--color-warning)", icon: Loader2 }, }; function formatRelativeTime(timestamp: string | undefined): string { if (!timestamp) return "Never"; const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } function truncatePath(path: string, maxLength: number = 40): string { if (path.length <= maxLength) return path; const start = path.slice(0, Math.floor(maxLength / 2) - 2); const end = path.slice(-Math.floor(maxLength / 2) + 2); return `${start}...${end}`; } function areProjectCardPropsEqual(previous: ProjectCardProps, next: ProjectCardProps): boolean { if (previous.project.id !== next.project.id) return false; if (previous.project.status !== next.project.status) return false; if (previous.project.name !== next.project.name) return false; if (previous.project.path !== next.project.path) return false; if (previous.project.lastActivityAt !== next.project.lastActivityAt) return false; if (previous.isLoading !== next.isLoading) return false; // Compare health const prevHealth = previous.health; const nextHealth = next.health; if (!prevHealth && !nextHealth) return true; if (!prevHealth || !nextHealth) return false; if ( prevHealth.activeTaskCount !== nextHealth.activeTaskCount || prevHealth.inFlightAgentCount !== nextHealth.inFlightAgentCount || prevHealth.totalTasksCompleted !== nextHealth.totalTasksCompleted || prevHealth.totalTasksFailed !== nextHealth.totalTasksFailed || prevHealth.status !== nextHealth.status ) { return false; } const prevMappings = previous.availabilityMappings ?? []; const nextMappings = next.availabilityMappings ?? []; if (prevMappings.length !== nextMappings.length) return false; return prevMappings.every((mapping, index) => { const nextMapping = nextMappings[index]; return Boolean(nextMapping) && mapping.nodeId === nextMapping.nodeId && mapping.path === nextMapping.path && mapping.displayName === nextMapping.displayName && mapping.available === nextMapping.available; }); } function ProjectCardInner({ project, health, onSelect, onPause, onResume, onRemove, availabilityMappings = [], isLoading = false, }: ProjectCardProps) { const [removeArmed, setRemoveArmed] = useState(false); const statusConfig = STATUS_CONFIG[project.status]; const StatusIcon = statusConfig.icon; const handleSelect = useCallback(() => { onSelect(project); }, [onSelect, project]); const handlePause = useCallback((e: React.MouseEvent) => { e.stopPropagation(); onPause(project); }, [onPause, project]); const handleResume = useCallback((e: React.MouseEvent) => { e.stopPropagation(); onResume(project); }, [onResume, project]); const handleRemove = useCallback((e: React.MouseEvent) => { e.stopPropagation(); if (!removeArmed) { setRemoveArmed(true); return; } onRemove(project); setRemoveArmed(false); }, [removeArmed, onRemove, project]); const isPaused = project.status === "paused"; const isErrored = project.status === "errored"; const isInitializing = project.status === "initializing"; return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleSelect(); } }} >

{project.name}

{availabilityMappings.length > 0 && (
{availabilityMappings.slice(0, 3).map((mapping) => (
{mapping.displayName} {truncatePath(mapping.path, 28)}
))} {availabilityMappings.length > 3 && ( +{availabilityMappings.length - 3} more )}
)} {truncatePath(project.path)}
{statusConfig.label}
{health && ( <>
{health.activeTaskCount} Active Tasks
{health.inFlightAgentCount} Agents
{health.totalTasksCompleted} Completed
)} {!health && (
No health data available
)}
Last activity: {formatRelativeTime(project.lastActivityAt || health?.lastActivityAt)}
{isPaused ? ( ) : ( )}
); } export const ProjectCard = memo(ProjectCardInner, areProjectCardPropsEqual);